feat(db/m7-user-mgmt): migration 0024 + self-service + SMTP queries (U3)
Adds users.email (optional, lowercase-unique via partial index), smtp_config singleton, and password_resets tokens. Plus the queries U3-T2 / T3 / T4 use: - ChangeUserPassword: self-service password change. HTTP layer verifies the current password before this fires. - UpdateUserProfile: set display_name + email together. - RegenerateApiToken: invalidate old API token. - GetUserByEmail: case-insensitive lookup for forgot-password. - GetSMTPConfig + UpdateSMTPConfig: admin SMTP settings CRUD. - CreatePasswordReset / GetPasswordReset / UsePasswordReset (:execrows for atomic claim) / DeleteExpiredPasswordResets (cron-style cleanup, not yet wired). Email column is nullable; users without email have admin-reset as their only password-recovery path. The lower() unique index allows many NULLs and prevents case-insensitive duplicates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: password_resets.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createPasswordReset = `-- name: CreatePasswordReset :exec
|
||||
INSERT INTO password_resets (token, user_id, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
`
|
||||
|
||||
type CreatePasswordResetParams struct {
|
||||
Token string
|
||||
UserID pgtype.UUID
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func (q *Queries) CreatePasswordReset(ctx context.Context, arg CreatePasswordResetParams) error {
|
||||
_, err := q.db.Exec(ctx, createPasswordReset, arg.Token, arg.UserID, arg.ExpiresAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteExpiredPasswordResets = `-- name: DeleteExpiredPasswordResets :exec
|
||||
DELETE FROM password_resets WHERE expires_at < now() - interval '7 days'
|
||||
`
|
||||
|
||||
// Cleanup: drop tokens that have been expired more than 7 days.
|
||||
// Not wired in U3; useful to have for a future cron sweep.
|
||||
func (q *Queries) DeleteExpiredPasswordResets(ctx context.Context) error {
|
||||
_, err := q.db.Exec(ctx, deleteExpiredPasswordResets)
|
||||
return err
|
||||
}
|
||||
|
||||
const getPasswordReset = `-- name: GetPasswordReset :one
|
||||
SELECT token, user_id, created_at, expires_at, used_at FROM password_resets WHERE token = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetPasswordReset(ctx context.Context, token string) (PasswordReset, error) {
|
||||
row := q.db.QueryRow(ctx, getPasswordReset, token)
|
||||
var i PasswordReset
|
||||
err := row.Scan(
|
||||
&i.Token,
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.ExpiresAt,
|
||||
&i.UsedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const usePasswordReset = `-- name: UsePasswordReset :execrows
|
||||
UPDATE password_resets
|
||||
SET used_at = now()
|
||||
WHERE token = $1
|
||||
AND used_at IS NULL
|
||||
AND expires_at > now()
|
||||
`
|
||||
|
||||
// Atomic claim: marks the token used only if currently unused
|
||||
// and not expired. Returns rows-affected so the caller can
|
||||
// distinguish "successful claim" (rows=1) from "already used /
|
||||
// expired / nonexistent" (rows=0).
|
||||
func (q *Queries) UsePasswordReset(ctx context.Context, token string) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, usePasswordReset, token)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user