186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
package postgres
|
|
|
|
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 {
|
|
return DB.QueryRowContext(ctx, `
|
|
INSERT INTO users (username, email, password_hash, display_name, role, is_active, settings)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_at, updated_at`,
|
|
u.Username, u.Email, u.PasswordHash, u.DisplayName, u.Role, u.IsActive, ToJSON(u.Settings),
|
|
).Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
|
|
}
|
|
|
|
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) {
|
|
return s.getBy(ctx, "username", username)
|
|
}
|
|
|
|
func (s *UserStore) GetByEmail(ctx context.Context, email string) (*models.User, error) {
|
|
return s.getBy(ctx, "email", email)
|
|
}
|
|
|
|
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 username = $1 OR email = $1`, login).Scan(
|
|
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
|
&u.Role, &u.IsActive, &settingsJSON, &u.CreatedAt, &u.UpdatedAt, &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 = $1", 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, &u.CreatedAt, &u.UpdatedAt, &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 = NOW() WHERE id = $1", id)
|
|
return err
|
|
}
|
|
|
|
func (s *UserStore) SetActive(ctx context.Context, id string, active bool) error {
|
|
_, err := DB.ExecContext(ctx, "UPDATE users SET is_active = $1 WHERE id = $2", 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 (user_id, token_hash, expires_at)
|
|
VALUES ($1, $2, $3)`, userID, tokenHash, expiresAt)
|
|
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 = $1 AND revoked_at IS NULL AND expires_at > 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 = NOW() WHERE token_hash = $1", tokenHash)
|
|
return err
|
|
}
|
|
|
|
func (s *UserStore) RevokeAllRefreshTokens(ctx context.Context, userID string) error {
|
|
_, err := DB.ExecContext(ctx,
|
|
"UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = $1 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 < NOW() - INTERVAL '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 = $1`, col), val).Scan(
|
|
&u.ID, &u.Username, &u.Email, &u.PasswordHash, &displayName, &avatarURL,
|
|
&u.Role, &u.IsActive, &settingsJSON, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.DisplayName = NullableString(displayName)
|
|
u.AvatarURL = NullableString(avatarURL)
|
|
ScanJSON(settingsJSON, &u.Settings)
|
|
return &u, nil
|
|
}
|