Files
minstrel/internal/db/migrations/0024_user_management_u3.up.sql
bvandeusen a509ec538b 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>
2026-05-07 12:34:44 -04:00

45 lines
1.8 KiB
SQL

-- User management U3: self-service profile + email + email-based
-- password reset infrastructure.
--
-- - users.email: optional; required for the forgot-password flow.
-- Lowercase-unique via a partial index to allow many NULL emails
-- while preventing case-insensitive duplicates.
-- - smtp_config singleton: SMTP server settings + from-address +
-- enabled flag. The actual mailer reads this row at send time so
-- config edits take effect without a server restart.
-- - password_resets: short-lived single-use tokens for the
-- forgot-password flow. 24h TTL enforced at issue time.
ALTER TABLE users ADD COLUMN IF NOT EXISTS email text;
CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique
ON users (lower(email))
WHERE email IS NOT NULL;
CREATE TABLE smtp_config (
id boolean PRIMARY KEY DEFAULT true,
enabled boolean NOT NULL DEFAULT false,
host text NOT NULL DEFAULT '',
port int NOT NULL DEFAULT 587,
username text NOT NULL DEFAULT '',
password text NOT NULL DEFAULT '',
from_address text NOT NULL DEFAULT '',
from_name text NOT NULL DEFAULT 'Minstrel',
use_tls boolean NOT NULL DEFAULT true,
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT smtp_config_singleton CHECK (id = true)
);
INSERT INTO smtp_config (id) VALUES (true)
ON CONFLICT (id) DO NOTHING;
CREATE TABLE password_resets (
token text PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
used_at timestamptz
);
CREATE INDEX password_resets_user_id_idx ON password_resets (user_id);
CREATE INDEX password_resets_active_idx
ON password_resets (expires_at)
WHERE used_at IS NULL;