Changeset 0.17.1 (#76)
This commit is contained in:
222
server/store/sqlite/user.go
Normal file
222
server/store/sqlite/user.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type UserStore struct{}
|
||||
|
||||
func NewUserStore() *UserStore { return &UserStore{} }
|
||||
|
||||
func (s *UserStore) Create(ctx context.Context, u *models.User) error {
|
||||
u.ID = store.NewID()
|
||||
now := time.Now().UTC()
|
||||
u.CreatedAt = now
|
||||
u.UpdatedAt = now
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO users (id, username, email, password_hash, display_name, role, is_active, settings, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
u.ID, u.Username, u.Email, u.PasswordHash, u.DisplayName, u.Role, u.IsActive, ToJSON(u.Settings),
|
||||
now.Format(timeFmt), now.Format(timeFmt),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByID(ctx context.Context, id string) (*models.User, error) {
|
||||
return s.getBy(ctx, "id", id)
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
var u models.User
|
||||
var displayName, avatarURL sql.NullString
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at
|
||||
FROM users WHERE LOWER(username) = LOWER(?)`, username).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
||||
&u.Role, &u.IsActive, &settingsJSON, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.DisplayName = NullableString(displayName)
|
||||
u.AvatarURL = NullableString(avatarURL)
|
||||
ScanJSON(settingsJSON, &u.Settings)
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByEmail(ctx context.Context, email string) (*models.User, error) {
|
||||
var u models.User
|
||||
var displayName, avatarURL sql.NullString
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at
|
||||
FROM users WHERE LOWER(email) = LOWER(?)`, email).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
||||
&u.Role, &u.IsActive, &settingsJSON, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.DisplayName = NullableString(displayName)
|
||||
u.AvatarURL = NullableString(avatarURL)
|
||||
ScanJSON(settingsJSON, &u.Settings)
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByLogin(ctx context.Context, login string) (*models.User, error) {
|
||||
var u models.User
|
||||
var displayName, avatarURL sql.NullString
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at
|
||||
FROM users WHERE LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)`, login, login).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
||||
&u.Role, &u.IsActive, &settingsJSON, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.DisplayName = NullableString(displayName)
|
||||
u.AvatarURL = NullableString(avatarURL)
|
||||
ScanJSON(settingsJSON, &u.Settings)
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) Update(ctx context.Context, id string, fields map[string]interface{}) error {
|
||||
b := NewUpdate("users")
|
||||
for k, v := range fields {
|
||||
if k == "settings" {
|
||||
b.SetJSON(k, v)
|
||||
} else {
|
||||
b.Set(k, v)
|
||||
}
|
||||
}
|
||||
if !b.HasSets() {
|
||||
return nil
|
||||
}
|
||||
b.Where("id", id)
|
||||
_, err := b.Exec(DB)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, "DELETE FROM users WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) List(ctx context.Context, opts store.ListOptions) ([]models.User, int, error) {
|
||||
b := NewSelect(
|
||||
"id, username, email, display_name, avatar_url, role, is_active, settings, created_at, updated_at, last_login_at",
|
||||
"users",
|
||||
)
|
||||
if opts.Sort == "" {
|
||||
b.OrderBy("username", "ASC")
|
||||
}
|
||||
b.Paginate(opts)
|
||||
|
||||
// Count
|
||||
var total int
|
||||
DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&total)
|
||||
|
||||
q, args := b.Build()
|
||||
rows, err := DB.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.User
|
||||
for rows.Next() {
|
||||
var u models.User
|
||||
var displayName, avatarURL sql.NullString
|
||||
var settingsJSON []byte
|
||||
err := rows.Scan(&u.ID, &u.Username, &u.Email, &displayName, &avatarURL,
|
||||
&u.Role, &u.IsActive, &settingsJSON, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
u.DisplayName = NullableString(displayName)
|
||||
u.AvatarURL = NullableString(avatarURL)
|
||||
ScanJSON(settingsJSON, &u.Settings)
|
||||
result = append(result, u)
|
||||
}
|
||||
return result, total, rows.Err()
|
||||
}
|
||||
|
||||
func (s *UserStore) UpdateLastLogin(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, "UPDATE users SET last_login_at = datetime('now') WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) SetActive(ctx context.Context, id string, active bool) error {
|
||||
_, err := DB.ExecContext(ctx, "UPDATE users SET is_active = ? WHERE id = ?", active, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Refresh Tokens ──────────────────────────
|
||||
|
||||
func (s *UserStore) CreateRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
|
||||
VALUES (?, ?, ?, ?)`, store.NewID(), userID, tokenHash, expiresAt.Format(timeFmt))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) GetRefreshToken(ctx context.Context, tokenHash string) (string, error) {
|
||||
var userID string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT user_id FROM refresh_tokens
|
||||
WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > datetime('now')`,
|
||||
tokenHash).Scan(&userID)
|
||||
return userID, err
|
||||
}
|
||||
|
||||
func (s *UserStore) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
"UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE token_hash = ?", tokenHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) RevokeAllRefreshTokens(ctx context.Context, userID string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
"UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL", userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserStore) CleanExpiredTokens(ctx context.Context) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
"DELETE FROM refresh_tokens WHERE expires_at < datetime('now', '-30 days')")
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Internal ────────────────────────────────
|
||||
|
||||
func (s *UserStore) getBy(ctx context.Context, col, val string) (*models.User, error) {
|
||||
var u models.User
|
||||
var displayName, avatarURL sql.NullString
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, fmt.Sprintf(`
|
||||
SELECT id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at
|
||||
FROM users WHERE %s = ?`, col), val).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
||||
&u.Role, &u.IsActive, &settingsJSON, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.DisplayName = NullableString(displayName)
|
||||
u.AvatarURL = NullableString(avatarURL)
|
||||
ScanJSON(settingsJSON, &u.Settings)
|
||||
return &u, nil
|
||||
}
|
||||
Reference in New Issue
Block a user