accounts and email
This commit is contained in:
@@ -49,16 +49,18 @@ func (r *Repository) UpsertUser(ctx context.Context, user User) (User, error) {
|
||||
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
var user User
|
||||
var created, lastSeen, passwordHash, role string
|
||||
var disabled int
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, email, COALESCE(display_name, ''), COALESCE(password_hash, ''), COALESCE(role, 'viewer'), created_at, COALESCE(last_seen_at, '')
|
||||
SELECT id, email, COALESCE(display_name, ''), COALESCE(password_hash, ''), COALESCE(role, 'viewer'), disabled, created_at, COALESCE(last_seen_at, '')
|
||||
FROM users
|
||||
WHERE email = ?
|
||||
`, email).Scan(&user.ID, &user.Email, &user.DisplayName, &passwordHash, &role, &created, &lastSeen)
|
||||
`, email).Scan(&user.ID, &user.Email, &user.DisplayName, &passwordHash, &role, &disabled, &created, &lastSeen)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
user.PasswordHash = passwordHash
|
||||
user.Role = Role(role)
|
||||
user.Disabled = disabled == 1
|
||||
user.CreatedAt, err = time.Parse(time.RFC3339, created)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("parse user created_at: %w", err)
|
||||
@@ -94,7 +96,7 @@ func (r *Repository) CountUsers(ctx context.Context) (int, error) {
|
||||
|
||||
func (r *Repository) CountAdmins(ctx context.Context) (int, error) {
|
||||
var count int
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin'`).Scan(&count); err != nil {
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin' AND disabled = 0`).Scan(&count); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
@@ -189,7 +191,7 @@ func (r *Repository) UpdateSignupsEnabled(ctx context.Context, enabled bool) err
|
||||
|
||||
func (r *Repository) ListUsers(ctx context.Context) ([]User, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, email, COALESCE(display_name, ''), COALESCE(password_hash, ''), COALESCE(role, 'viewer'), created_at, COALESCE(last_seen_at, '')
|
||||
SELECT id, email, COALESCE(display_name, ''), COALESCE(password_hash, ''), COALESCE(role, 'viewer'), disabled, created_at, COALESCE(last_seen_at, '')
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
@@ -202,10 +204,12 @@ func (r *Repository) ListUsers(ctx context.Context) ([]User, error) {
|
||||
for rows.Next() {
|
||||
var user User
|
||||
var created, lastSeen, role string
|
||||
if err := rows.Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, &role, &created, &lastSeen); err != nil {
|
||||
var disabled int
|
||||
if err := rows.Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, &role, &disabled, &created, &lastSeen); err != nil {
|
||||
return nil, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
user.Role = Role(role)
|
||||
user.Disabled = disabled == 1
|
||||
user.CreatedAt, _ = time.Parse(time.RFC3339, created)
|
||||
if lastSeen != "" {
|
||||
user.LastSeenAt, _ = time.Parse(time.RFC3339, lastSeen)
|
||||
@@ -245,6 +249,41 @@ func (r *Repository) UpdateUserRole(ctx context.Context, userID string, role Rol
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateUserAccess(ctx context.Context, updates []UserAccessUpdate) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin user access update: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
for _, update := range updates {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
UPDATE users
|
||||
SET role = ?, disabled = ?
|
||||
WHERE id = ?
|
||||
`, string(update.Role), boolInt(update.Disabled), update.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update user access: %w", err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
}
|
||||
|
||||
var admins int
|
||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin' AND disabled = 0`).Scan(&admins); err != nil {
|
||||
return fmt.Errorf("count enabled admins: %w", err)
|
||||
}
|
||||
if admins == 0 {
|
||||
return fmt.Errorf("cannot disable or demote the last admin account")
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit user access update: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteUser(ctx context.Context, userID string) error {
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id = ?`, userID)
|
||||
if err != nil {
|
||||
@@ -411,6 +450,87 @@ func (r *Repository) RevokeSession(ctx context.Context, sessionID string) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) CreatePasswordResetToken(ctx context.Context, token PasswordResetToken) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin password reset token: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE password_reset_tokens
|
||||
SET used_at = ?
|
||||
WHERE user_id = ? AND used_at IS NULL
|
||||
`, now, token.UserID); err != nil {
|
||||
return fmt.Errorf("expire prior password reset tokens: %w", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO password_reset_tokens (id, user_id, token_hash, created_at, expires_at, initiated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, token.ID, token.UserID, token.TokenHash, token.CreatedAt.Format(time.RFC3339), token.ExpiresAt.Format(time.RFC3339), nullString(token.InitiatedBy)); err != nil {
|
||||
return fmt.Errorf("create password reset token: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit password reset token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ExpirePasswordResetToken(ctx context.Context, tokenHash string) {
|
||||
_, _ = r.db.ExecContext(ctx, `
|
||||
UPDATE password_reset_tokens
|
||||
SET used_at = ?
|
||||
WHERE token_hash = ? AND used_at IS NULL
|
||||
`, time.Now().UTC().Format(time.RFC3339), tokenHash)
|
||||
}
|
||||
|
||||
func (r *Repository) CompletePasswordReset(ctx context.Context, tokenHash, passwordHash string) (string, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("begin password reset: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var id, userID, expires, used string
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, expires_at, COALESCE(used_at, '')
|
||||
FROM password_reset_tokens
|
||||
WHERE token_hash = ?
|
||||
`, tokenHash).Scan(&id, &userID, &expires, &used); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if used != "" {
|
||||
return "", fmt.Errorf("password reset link has already been used")
|
||||
}
|
||||
expiresAt, err := time.Parse(time.RFC3339, expires)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse password reset expiry: %w", err)
|
||||
}
|
||||
if time.Now().UTC().After(expiresAt) {
|
||||
return "", fmt.Errorf("password reset link has expired")
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
result, err := tx.ExecContext(ctx, `UPDATE password_reset_tokens SET used_at = ? WHERE id = ? AND used_at IS NULL`, now, id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("consume password reset token: %w", err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return "", fmt.Errorf("password reset link has already been used")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE users SET password_hash = ? WHERE id = ?`, passwordHash, userID); err != nil {
|
||||
return "", fmt.Errorf("update reset password: %w", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL`, now, userID); err != nil {
|
||||
return "", fmt.Errorf("revoke password reset sessions: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", fmt.Errorf("commit password reset: %w", err)
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateAPIKey(ctx context.Context, key APIKey) error {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO api_keys (id, user_id, name, key_hash, scopes, created_at, expires_at, last_used_at, revoked_at)
|
||||
|
||||
Reference in New Issue
Block a user