diff --git a/TODO.md b/TODO.md index 3db4144..8f39e06 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,9 @@ # TODO +## App permissions + +- [ ] **Permissions registry** — admin-managed table that controls which apps each user can access. Schema: `user_app_permissions (user_id FK, app_key)`. Admin UI lets the admin grant/revoke per-app access per user. The Apps page only shows apps the current user has been granted access to. + ## Frontend features - [x] **Logout button** — visible when logged in, clears token and redirects to `/login` diff --git a/backend/scripts/seed.py b/backend/scripts/seed.py index bd25807..75565a1 100644 --- a/backend/scripts/seed.py +++ b/backend/scripts/seed.py @@ -1,4 +1,8 @@ -"""Create a test user for the dev environment if it doesn't exist yet.""" +"""Seed the dev environment with a fixed set of test users. + +Users are upserted on every startup — missing ones are created, existing ones +are left untouched except for the admin flag which is always enforced. +""" import asyncio @@ -8,35 +12,62 @@ from app.core.security import hash_password from app.database import AsyncSessionLocal from app.models.user import User -TEST_EMAIL = "test@example.com" -TEST_PASSWORD = "Test123!" -TEST_NAME = "Test User" +# ── Dev seed users ──────────────────────────────────────────────────────────── +# Passwords satisfy the strength policy (upper, lower, digit, special char, +# no forbidden words) so they can also be used via the API if needed. + +SEED_USERS = [ + { + "email": "test_admin@example.com", + "password": "Secure_Dev1!", + "full_name": "Test Admin", + "is_superuser": True, + }, + { + "email": "test_1@example.com", + "password": "Secure_Dev2!", + "full_name": "Test User One", + "is_superuser": False, + }, + { + "email": "test_2@example.com", + "password": "Secure_Dev3!", + "full_name": "Test User Two", + "is_superuser": False, + }, +] async def seed() -> None: async with AsyncSessionLocal() as db: - result = await db.execute(select(User).where(User.email == TEST_EMAIL)) - existing = result.scalar_one_or_none() + for spec in SEED_USERS: + result = await db.execute( + select(User).where(User.email == spec["email"]) + ) + existing = result.scalar_one_or_none() - if existing: - # Ensure the dev test user is always an admin - if not existing.is_superuser: - existing.is_superuser = True - await db.commit() - print(f"[seed] promoted test user to admin: {TEST_EMAIL}") + if existing: + # Always enforce the correct admin flag in case it drifted + if existing.is_superuser != spec["is_superuser"]: + existing.is_superuser = spec["is_superuser"] + await db.commit() + flag = "admin" if spec["is_superuser"] else "user" + print(f"[seed] updated role → {flag}: {spec['email']}") + else: + print(f"[seed] already exists: {spec['email']}") else: - print(f"[seed] test user already exists: {TEST_EMAIL}") - return - - user = User( - email=TEST_EMAIL, - hashed_password=hash_password(TEST_PASSWORD), - full_name=TEST_NAME, - is_superuser=True, - ) - db.add(user) - await db.commit() - print(f"[seed] created test admin — email: {TEST_EMAIL} pwd: {TEST_PASSWORD}") + user = User( + email=spec["email"], + hashed_password=hash_password(spec["password"]), + full_name=spec["full_name"], + is_superuser=spec["is_superuser"], + ) + db.add(user) + await db.commit() + role = "admin" if spec["is_superuser"] else "user" + print( + f"[seed] created {role}: {spec['email']} pwd: {spec['password']}" + ) if __name__ == "__main__":