UMBRA/backend/alembic/versions/028_add_user_id_to_reminders.py
Kyle Pope d8bdae8ec3 Implement multi-user RBAC: database, auth, routing, admin API (Phases 1-6)
Phase 1: Add role, mfa_enforce_pending, must_change_password to users table.
Create system_config (singleton) and audit_log tables. Migration 026.

Phase 2: Add user_id FK to all 8 data tables (todos, reminders, projects,
calendars, people, locations, event_templates, ntfy_sent) with 4-step
nullable→backfill→FK→NOT NULL pattern. Migrations 027-034.

Phase 3: Harden auth schemas (extra="forbid" on RegisterRequest), add
MFA enforcement token serializer with distinct salt, rewrite auth router
with require_role() factory and registration endpoint.

Phase 4: Scope all 12 routers by user_id, fix dependency type bugs,
bound weather cache (SEC-15), multi-user ntfy dispatch.

Phase 5: Create admin router (14 endpoints), admin schemas, audit
service, rate limiting in nginx. SEC-08 CSRF via X-Requested-With.

Phase 6: Update frontend types, useAuth hook (role/isAdmin/register),
App.tsx (AdminRoute guard), Sidebar (admin link), api.ts (XHR header).

Security findings addressed: SEC-01, SEC-02, SEC-03, SEC-04, SEC-05,
SEC-06, SEC-07, SEC-08, SEC-12, SEC-13, SEC-15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:06:25 +08:00

37 lines
1.1 KiB
Python

"""Add user_id FK to reminders table.
Revision ID: 028
Revises: 027
Create Date: 2026-02-26
"""
from alembic import op
import sqlalchemy as sa
revision = "028"
down_revision = "027"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("reminders", sa.Column("user_id", sa.Integer(), nullable=True))
op.execute(
"UPDATE reminders SET user_id = ("
" SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1"
")"
)
op.create_foreign_key(
"fk_reminders_user_id", "reminders", "users",
["user_id"], ["id"], ondelete="CASCADE"
)
op.alter_column("reminders", "user_id", nullable=False)
op.create_index("ix_reminders_user_id", "reminders", ["user_id"])
op.create_index("ix_reminders_user_remind_at", "reminders", ["user_id", "remind_at"])
def downgrade() -> None:
op.drop_index("ix_reminders_user_remind_at", table_name="reminders")
op.drop_index("ix_reminders_user_id", table_name="reminders")
op.drop_constraint("fk_reminders_user_id", "reminders", type_="foreignkey")
op.drop_column("reminders", "user_id")