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:
2026-05-07 12:34:44 -04:00
parent 777e32ca24
commit a509ec538b
9 changed files with 411 additions and 8 deletions
+28
View File
@@ -114,3 +114,31 @@ UPDATE users
SET auto_approve_requests = $2
WHERE id = $1
RETURNING *;
-- name: ChangeUserPassword :exec
-- Self-service password change. Caller (HTTP handler) verifies the
-- current password before calling this. Distinct from
-- ResetUserPassword which is admin-driven (no current-password
-- check).
UPDATE users SET password_hash = $2 WHERE id = $1;
-- name: UpdateUserProfile :one
-- Self-service: set display name and/or email. Both fields are
-- nullable in the DB; pass NULL ptr to clear, non-NULL ptr to set.
UPDATE users
SET display_name = $2,
email = $3
WHERE id = $1
RETURNING *;
-- name: RegenerateApiToken :one
-- Self-service: caller wants a new API token. Used by the /settings
-- API Token card's "Regenerate" button.
UPDATE users SET api_token = $2 WHERE id = $1
RETURNING *;
-- name: GetUserByEmail :one
-- Used by forgot-password lookup. Lowercase comparison both sides
-- to keep the unique index happy and to make the lookup
-- case-insensitive.
SELECT * FROM users WHERE lower(email) = lower($1);