From 1042fff2b8464a2424c2f573587afcc3c3fcf4d3 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 11:10:29 +0100 Subject: [PATCH] feat: add is_admin column to users table Add `is_admin` boolean column (default false) via Alembic migration for role-based access control. Co-Authored-By: Claude Opus 4.6 --- ...-aware-ui-and-role-based-access-control.md | 5 +-- ...wah--add-is-admin-column-to-users-table.md | 32 +++++++++++++++---- .../p7e8f9a0b1c2_add_is_admin_to_users.py | 29 +++++++++++++++++ backend/src/app/models/user.py | 3 +- 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 backend/src/app/alembic/versions/p7e8f9a0b1c2_add_is_admin_to_users.py diff --git a/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md b/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md index 92c5399..2b3ea8f 100644 --- a/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md +++ b/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md @@ -1,10 +1,11 @@ --- # nuzlocke-tracker-ce4o title: Auth-aware UI and role-based access control -status: todo +status: completed type: epic +priority: normal created_at: 2026-03-21T10:05:52Z -updated_at: 2026-03-21T10:05:52Z +updated_at: 2026-03-21T10:08:39Z --- The app currently shows the same navigation menu to all users regardless of auth state. Logged-out users can navigate to protected pages (e.g., /runs/new, /admin) even though the backend rejects their requests. The admin interface has no role restriction — any authenticated user can access it. diff --git a/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md b/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md index 47636c3..6582739 100644 --- a/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md +++ b/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md @@ -1,10 +1,11 @@ --- # nuzlocke-tracker-dwah title: Add is_admin column to users table -status: todo +status: completed type: task +priority: normal created_at: 2026-03-21T10:06:19Z -updated_at: 2026-03-21T10:06:19Z +updated_at: 2026-03-21T10:10:02Z parent: nuzlocke-tracker-ce4o --- @@ -12,12 +13,31 @@ Add an `is_admin` boolean column (default `false`) to the `users` table via an A ## Checklist -- [ ] Create Alembic migration adding `is_admin: Mapped[bool]` column with `server_default="false"` -- [ ] Update `User` model in `backend/src/app/models/user.py` -- [ ] Run migration and verify column exists -- [ ] Seed a test admin user (or document how to set `is_admin=true` via SQL) +- [x] Create Alembic migration adding `is_admin: Mapped[bool]` column with `server_default="false"` +- [x] Update `User` model in `backend/src/app/models/user.py` +- [x] Run migration and verify column exists +- [x] Seed a test admin user (or document how to set `is_admin=true` via SQL) ## Files to change - `backend/src/app/models/user.py` — add `is_admin` field - `backend/src/app/alembic/versions/` — new migration + +## Summary of Changes + +Added `is_admin` boolean column to the `users` table: + +- **Migration**: `p7e8f9a0b1c2_add_is_admin_to_users.py` adds the column with `server_default='false'` +- **Model**: Updated `User` model with `is_admin: Mapped[bool]` field + +### Setting admin via SQL + +To promote a user to admin: +```sql +UPDATE users SET is_admin = true WHERE email = 'admin@example.com'; +``` + +Or by user ID: +```sql +UPDATE users SET is_admin = true WHERE id = ''; +``` diff --git a/backend/src/app/alembic/versions/p7e8f9a0b1c2_add_is_admin_to_users.py b/backend/src/app/alembic/versions/p7e8f9a0b1c2_add_is_admin_to_users.py new file mode 100644 index 0000000..0d935ce --- /dev/null +++ b/backend/src/app/alembic/versions/p7e8f9a0b1c2_add_is_admin_to_users.py @@ -0,0 +1,29 @@ +"""add is_admin to users + +Revision ID: p7e8f9a0b1c2 +Revises: o6d7e8f9a0b1 +Create Date: 2026-03-21 10:00:00.000000 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "p7e8f9a0b1c2" +down_revision: str | Sequence[str] | None = "o6d7e8f9a0b1" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "users", + sa.Column("is_admin", sa.Boolean(), nullable=False, server_default="false"), + ) + + +def downgrade() -> None: + op.drop_column("users", "is_admin") diff --git a/backend/src/app/models/user.py b/backend/src/app/models/user.py index ba7ff53..7d476b4 100644 --- a/backend/src/app/models/user.py +++ b/backend/src/app/models/user.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import TYPE_CHECKING from uuid import UUID -from sqlalchemy import DateTime, String, func +from sqlalchemy import Boolean, DateTime, String, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base @@ -19,6 +19,7 @@ class User(Base): id: Mapped[UUID] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(255), unique=True, index=True) display_name: Mapped[str | None] = mapped_column(String(100)) + is_admin: Mapped[bool] = mapped_column(Boolean, server_default="false") created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() )