From c20e9e3050520e0e7c259b2d87882def1a6c8223 Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:32:51 +0700 Subject: [PATCH 1/6] docs: design normalized database seed update --- ...6-06-22-normalized-database-seed-design.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-normalized-database-seed-design.md diff --git a/docs/superpowers/specs/2026-06-22-normalized-database-seed-design.md b/docs/superpowers/specs/2026-06-22-normalized-database-seed-design.md new file mode 100644 index 0000000..8c97253 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-normalized-database-seed-design.md @@ -0,0 +1,64 @@ +# Normalized Database Seed Design + +## Goal + +Update user seeding to work with the normalized user database schema while preserving the existing seed configuration and idempotent behavior. Update the README so operators can understand which records are created and how `SEED_ADMIN_FULLNAME` maps to the current schema. + +## Current Problem + +The user seeder passes `fullname` to `User.create()`. The normalized `users` table and current domain factory no longer accept that field. Personal names now belong in `user_profiles`, specifically `display_name` for the existing seed value. As a result, configured user seeding fails before a user can be persisted. + +## Design + +### User creation + +The seeder will create each user with the current identity fields only: email, password hash, and username. It will continue using `SQLAlchemyUserRepository.save()`, which creates the required default `user_profiles`, `user_settings`, and `user_security` records alongside a new user. + +### Profile population + +The seed repository contract will expose profile persistence. After saving a new user, the seeder will save a `UserProfile` whose `display_name` contains the configured full name. `SEED_ADMIN_FULLNAME` remains unchanged for backward compatibility. Development-user full names follow the same mapping. + +The profile is saved before assigning the role. All operations remain within the seed runner's existing transaction, so a failure rolls back the user, normalized related records, profile update, and role assignment together. + +### Idempotency + +Email remains the identity used to detect existing seed users. If a user already exists, the seeder will not update identity data, profile data, password hashes, settings, security state, or role assignments. Authorization resource, role, permission, role-permission, and Casbin policy seeding retains its existing idempotency checks. + +### Configuration + +No environment-variable names change. The relevant variables remain: + +- `SEED_ADMIN_EMAIL` +- `SEED_ADMIN_PASSWORD` +- `SEED_ADMIN_USERNAME` +- `SEED_ADMIN_FULLNAME` +- `SEED_DEVELOPMENT_USERS_PASSWORD` + +`SEED_ADMIN_FULLNAME` is documented as the value stored in `user_profiles.display_name`, not a column in `users`. + +## Testing + +Focused unit tests will cover: + +- Creating an admin user with current `User.create()` arguments. +- Saving the admin display name to a normalized profile. +- Creating development users and their profiles only in development mode. +- Skipping user and profile writes when seed credentials are missing or the email exists. +- Assigning the configured role only after a new user and profile are saved. +- Returning accurate user and role counters. + +Existing project tests and Ruff checks will be run after implementation. Database-backed execution will be used only if the configured local services are available; otherwise unit tests will verify the seeder's behavior through its repository and authorization contracts. + +## README Changes + +The database-seeding section will explain: + +- The authorization records created by the baseline seed. +- The normalized user records created for each new seeded user: `users`, `user_profiles`, `user_settings`, and `user_security`. +- The mapping from `SEED_ADMIN_FULLNAME` to `user_profiles.display_name`. +- Development-only demo accounts and their roles. +- Existing-user behavior and transactional/idempotent guarantees. + +## Scope + +This change does not alter migrations, database models, seed account credentials, authorization definitions, or existing-user reconciliation. It fixes user seed compatibility with the current schema and documents actual behavior. From 5fb3a1296b99b2afd30c10b0c5adae877f4fcab1 Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:41:18 +0700 Subject: [PATCH 2/6] docs: plan normalized database seed update --- .../2026-06-22-normalized-database-seed.md | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-22-normalized-database-seed.md diff --git a/docs/superpowers/plans/2026-06-22-normalized-database-seed.md b/docs/superpowers/plans/2026-06-22-normalized-database-seed.md new file mode 100644 index 0000000..9d62c46 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-normalized-database-seed.md @@ -0,0 +1,387 @@ +# Normalized Database Seed Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make configured user seeds compatible with the normalized user schema and document exactly which authorization and user records the seed creates. + +**Architecture:** Keep orchestration in `src/core/seed/user.py` behind its repository and authorization protocols. Create identity data through the current `User` factory, then persist the legacy full-name setting as `UserProfile.display_name`; the existing SQLAlchemy repository remains responsible for default profile, settings, and security rows and the runner retains transaction ownership. + +**Tech Stack:** Python 3.14, dataclasses, async repository protocols, SQLAlchemy async repository adapter, pytest, Ruff, Markdown. + +--- + +## File Structure + +- Create `tests/test_user_seed.py`: unit tests for normalized user/profile seeding, environment gating, idempotency, role assignment, and counters. +- Modify `src/core/seed/user.py`: correct the `User.create()` call and persist seed names through `UserProfile`. +- Modify `README.md`: document authorization records, normalized user records, configuration mapping, transaction behavior, and existing-user behavior. + +### Task 1: Add normalized user seed contract tests + +**Files:** +- Create: `tests/test_user_seed.py` + +- [ ] **Step 1: Write test fakes and a failing admin seed test** + +Create `tests/test_user_seed.py` with: + +```python +import asyncio +from uuid import uuid4 + +from src.core.seed.user import SeedUserConfig, seed_user +from src.modules.authorization.domain.permissions import ( + ADMIN_ROLE, + DEFAULT_USER_ROLE, + MANAGER_ROLE, + VIEWER_ROLE, +) +from src.modules.user.domain.entities.user import User, UserProfile + + +class FakeUserRepository: + def __init__(self, existing_users: tuple[User, ...] = ()) -> None: + self.users = {user.email: user for user in existing_users} + self.saved_users: list[User] = [] + self.saved_profiles: list[UserProfile] = [] + + async def get_by_email(self, email: str) -> User | None: + return self.users.get(email) + + async def save(self, user: User) -> User: + self.users[user.email] = user + self.saved_users.append(user) + return user + + async def save_profile(self, profile: UserProfile) -> UserProfile: + self.saved_profiles.append(profile) + return profile + + +class FakeAuthorizationService: + def __init__(self) -> None: + self.assignments: list[tuple[str, str]] = [] + + async def assign_role(self, subject: str, role: str) -> None: + self.assignments.append((subject, role)) + + +def test_seed_user_creates_admin_with_normalized_profile(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="admin@example.com", + admin_password="secret-password", + admin_username="admin", + admin_fullname="System Administrator", + ), + ) + ) + + assert result.users_created == 1 + assert result.roles_assigned == 1 + assert len(repository.saved_users) == 1 + saved_user = repository.saved_users[0] + assert saved_user.email == "admin@example.com" + assert saved_user.username == "admin" + assert saved_user.password_hash == "hashed:secret-password" + assert repository.saved_profiles == [ + UserProfile(user_id=saved_user.id, display_name="System Administrator") + ] + assert authorization.assignments == [(str(saved_user.id), ADMIN_ROLE)] +``` + +- [ ] **Step 2: Run the admin test to verify the current factory mismatch fails** + +Run: `.venv/bin/pytest tests/test_user_seed.py::test_seed_user_creates_admin_with_normalized_profile -v` + +Expected: FAIL with `TypeError` because `User.create()` currently receives unsupported `password` and `fullname` keyword arguments. + +- [ ] **Step 3: Add failing development, existing-user, and missing-credentials tests** + +Append: + +```python +def test_seed_user_creates_development_users_and_profiles(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="development", + admin_email="", + admin_password="", + development_users_password="demo-password", + ), + ) + ) + + assert result.users_created == 3 + assert result.roles_assigned == 3 + assert [user.email for user in repository.saved_users] == [ + "user@example.com", + "manager@example.com", + "viewer@example.com", + ] + assert [profile.display_name for profile in repository.saved_profiles] == [ + "Default User", + "Todo Manager", + "Todo Viewer", + ] + assert [role for _, role in authorization.assignments] == [ + DEFAULT_USER_ROLE, + MANAGER_ROLE, + VIEWER_ROLE, + ] + + +def test_seed_user_does_not_modify_an_existing_user(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + existing_user = User( + id=uuid4(), + email="admin@example.com", + password_hash="existing-hash", + username="existing-admin", + ) + repository = FakeUserRepository((existing_user,)) + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="admin@example.com", + admin_password="new-password", + admin_username="admin", + admin_fullname="System Administrator", + ), + ) + ) + + assert result.users_created == 0 + assert result.roles_assigned == 0 + assert repository.saved_users == [] + assert repository.saved_profiles == [] + assert authorization.assignments == [] + assert repository.users[existing_user.email] == existing_user + + +def test_seed_user_skips_users_without_credentials(): + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="", + admin_password="", + ), + ) + ) + + assert result.users_created == 0 + assert result.roles_assigned == 0 + assert repository.saved_users == [] + assert repository.saved_profiles == [] + assert authorization.assignments == [] +``` + +- [ ] **Step 4: Run the test file** + +Run: `.venv/bin/pytest tests/test_user_seed.py -v` + +Expected: missing-credentials and existing-user tests PASS; admin and development creation tests FAIL at the outdated `User.create()` call. + +- [ ] **Step 5: Commit the failing tests** + +```bash +git add tests/test_user_seed.py +git commit -m "test: cover normalized user seeding" +``` + +### Task 2: Persist seeded names through normalized profiles + +**Files:** +- Modify: `src/core/seed/user.py:1-161` +- Test: `tests/test_user_seed.py` + +- [ ] **Step 1: Import `UserProfile` and extend the seed repository contract** + +```python +from src.modules.user.domain.entities.user import User, UserProfile + + +class SeedUserRepository(Protocol): + async def get_by_email(self, email: str) -> User | None: + raise NotImplementedError + + async def save(self, user: User) -> User: + raise NotImplementedError + + async def save_profile(self, profile: UserProfile) -> UserProfile: + raise NotImplementedError +``` + +- [ ] **Step 2: Correct user creation and save the normalized profile before role assignment** + +Replace the creation block in `_seed_one_user()` with: + +```python + user = User.create( + email=email, + password_hash=PasswordSerrvice.hash(password), + username=username, + ) + saved_user = await user_repository.save(user) + await user_repository.save_profile( + UserProfile( + user_id=saved_user.id, + display_name=fullname, + ) + ) + await authorization_service.assign_role(str(saved_user.id), role) +``` + +This preserves ordering inside the runner transaction: identity and defaults, profile value, then role assignment. + +- [ ] **Step 3: Run seed tests** + +Run: `.venv/bin/pytest tests/test_user_seed.py -v` + +Expected: 4 tests PASS. + +- [ ] **Step 4: Run all tests** + +Run: `.venv/bin/pytest -q` + +Expected: all tests PASS. + +- [ ] **Step 5: Run focused lint** + +Run: `.venv/bin/ruff check src/core/seed/user.py tests/test_user_seed.py` + +Expected: `All checks passed!` + +- [ ] **Step 6: Commit the implementation** + +```bash +git add src/core/seed/user.py +git commit -m "fix: align user seed with normalized schema" +``` + +### Task 3: Document normalized seed behavior + +**Files:** +- Modify: `README.md:431-462` + +- [ ] **Step 1: Replace the database-seeding section with schema-accurate text** + +The revised section must state: + +````markdown +Seed baseline records after applying migrations: + +```bash +make seed +``` + +The seeder runs all changes in one transaction and is idempotent. It creates the default authorization resources, roles (`admin`, `user`, `manager`, and `viewer`), permissions, role-permission links, and matching Casbin policies without duplicating existing records. + +For each new seeded user, the repository creates records that follow the normalized user schema: + +- `users` stores email, username, password hash, authentication provider, and status. +- `user_profiles` stores `SEED_ADMIN_FULLNAME` (or the demo name) as `display_name`. +- `user_settings` stores default preferences. +- `user_security` stores default security state. +- `user_has_roles` associates the user with its seeded role. + +`SEED_ADMIN_FULLNAME` is retained for configuration compatibility; there is no `users.fullname` column. Existing users are not modified. +```` + +Keep the existing environment examples, add display names to the demo-account list, and explicitly describe transaction rollback and production/development gating. + +- [ ] **Step 2: Search for obsolete seed claims** + +Run: `rg -n "SEED_ADMIN_FULLNAME|fullname|default .*roles|user_profiles|user_settings|user_security" README.md src/core/seed` + +Expected: README maps `SEED_ADMIN_FULLNAME` to `user_profiles.display_name`; source uses `fullname` only as seed input; README lists all four roles. + +- [ ] **Step 3: Check whitespace** + +Run: `git diff --check` + +Expected: no output and exit status 0. + +- [ ] **Step 4: Commit README changes** + +```bash +git add README.md +git commit -m "docs: explain normalized database seeding" +``` + +### Task 4: Verify the complete change + +**Files:** +- Verify: `src/core/seed/user.py` +- Verify: `tests/test_user_seed.py` +- Verify: `README.md` + +- [ ] **Step 1: Run the full test suite** + +Run: `.venv/bin/pytest -q` + +Expected: all tests PASS. + +- [ ] **Step 2: Run project lint** + +Run: `.venv/bin/ruff check src tests scripts` + +Expected: `All checks passed!` + +- [ ] **Step 3: Run import-boundary checks** + +Run: `.venv/bin/lint-imports` + +Expected: all configured contracts are kept. + +- [ ] **Step 4: Verify application imports** + +Run: `PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -c "import src.main; print('import ok')"` + +Expected: `import ok`. + +- [ ] **Step 5: Inspect the final scoped diff and worktree** + +Run: + +```bash +git diff HEAD~3 -- src/core/seed/user.py tests/test_user_seed.py README.md +git status --short +``` + +Expected: only the planned seed, tests, and README changes appear; the pre-existing untracked `.vscode/PythonImportHelper-v2-Completion.json` remains untouched. From 38df3a8b88e627cf4f97e472e0dc3cc47f1c6606 Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:41:18 +0700 Subject: [PATCH 3/6] chore: ignore local worktrees --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b85f2b7..b468f1c 100644 --- a/.gitignore +++ b/.gitignore @@ -217,4 +217,7 @@ marimo/_lsp/ __marimo__/ # Streamlit -.streamlit/secrets.toml \ No newline at end of file +.streamlit/secrets.toml + +# Isolated development worktrees +.worktrees/ From 8797e85ffa457c30ea7e850da40850fcd02db3a1 Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:44:24 +0700 Subject: [PATCH 4/6] test: cover normalized user seeding --- tests/test_user_seed.py | 172 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/test_user_seed.py diff --git a/tests/test_user_seed.py b/tests/test_user_seed.py new file mode 100644 index 0000000..740b6bf --- /dev/null +++ b/tests/test_user_seed.py @@ -0,0 +1,172 @@ +import asyncio +from uuid import uuid4 + +from src.core.seed.user import SeedUserConfig, seed_user +from src.modules.authorization.domain.permissions import ( + ADMIN_ROLE, + DEFAULT_USER_ROLE, + MANAGER_ROLE, + VIEWER_ROLE, +) +from src.modules.user.domain.entities.user import User, UserProfile + + +class FakeUserRepository: + def __init__(self, existing_users: tuple[User, ...] = ()) -> None: + self.users = {user.email: user for user in existing_users} + self.saved_users: list[User] = [] + self.saved_profiles: list[UserProfile] = [] + + async def get_by_email(self, email: str) -> User | None: + return self.users.get(email) + + async def save(self, user: User) -> User: + self.users[user.email] = user + self.saved_users.append(user) + return user + + async def save_profile(self, profile: UserProfile) -> UserProfile: + self.saved_profiles.append(profile) + return profile + + +class FakeAuthorizationService: + def __init__(self) -> None: + self.assignments: list[tuple[str, str]] = [] + + async def assign_role(self, subject: str, role: str) -> None: + self.assignments.append((subject, role)) + + +def test_seed_user_creates_admin_with_normalized_profile(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="admin@example.com", + admin_password="secret-password", + admin_username="admin", + admin_fullname="System Administrator", + ), + ) + ) + + assert result.users_created == 1 + assert result.roles_assigned == 1 + assert len(repository.saved_users) == 1 + saved_user = repository.saved_users[0] + assert saved_user.email == "admin@example.com" + assert saved_user.username == "admin" + assert saved_user.password_hash == "hashed:secret-password" + assert repository.saved_profiles == [ + UserProfile(user_id=saved_user.id, display_name="System Administrator") + ] + assert authorization.assignments == [(str(saved_user.id), ADMIN_ROLE)] + + +def test_seed_user_creates_development_users_and_profiles(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="development", + admin_email="", + admin_password="", + development_users_password="demo-password", + ), + ) + ) + + assert result.users_created == 3 + assert result.roles_assigned == 3 + assert [user.email for user in repository.saved_users] == [ + "user@example.com", + "manager@example.com", + "viewer@example.com", + ] + assert [profile.display_name for profile in repository.saved_profiles] == [ + "Default User", + "Todo Manager", + "Todo Viewer", + ] + assert [role for _, role in authorization.assignments] == [ + DEFAULT_USER_ROLE, + MANAGER_ROLE, + VIEWER_ROLE, + ] + + +def test_seed_user_does_not_modify_an_existing_user(monkeypatch): + monkeypatch.setattr( + "src.core.seed.user.PasswordSerrvice.hash", + lambda password: f"hashed:{password}", + ) + existing_user = User( + id=uuid4(), + email="admin@example.com", + password_hash="existing-hash", + username="existing-admin", + ) + repository = FakeUserRepository((existing_user,)) + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="admin@example.com", + admin_password="new-password", + admin_username="admin", + admin_fullname="System Administrator", + ), + ) + ) + + assert result.users_created == 0 + assert result.roles_assigned == 0 + assert repository.saved_users == [] + assert repository.saved_profiles == [] + assert authorization.assignments == [] + assert repository.users[existing_user.email] == existing_user + + +def test_seed_user_skips_users_without_credentials(): + repository = FakeUserRepository() + authorization = FakeAuthorizationService() + + result = asyncio.run( + seed_user( + user_repository=repository, + authorization_service=authorization, + config=SeedUserConfig( + app_env="production", + admin_email="", + admin_password="", + ), + ) + ) + + assert result.users_created == 0 + assert result.roles_assigned == 0 + assert repository.saved_users == [] + assert repository.saved_profiles == [] + assert authorization.assignments == [] From bdbc4f6a938e59c7f5c043769f88dfabb3b4f79c Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:44:42 +0700 Subject: [PATCH 5/6] fix: align user seed with normalized schema --- src/core/seed/user.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/core/seed/user.py b/src/core/seed/user.py index a088c97..6bcbe2c 100644 --- a/src/core/seed/user.py +++ b/src/core/seed/user.py @@ -8,7 +8,7 @@ MANAGER_ROLE, VIEWER_ROLE, ) -from src.modules.user.domain.entities.user import User +from src.modules.user.domain.entities.user import User, UserProfile class SeedUserRepository(Protocol): @@ -18,6 +18,9 @@ async def get_by_email(self, email: str) -> User | None: async def save(self, user: User) -> User: raise NotImplementedError + async def save_profile(self, profile: UserProfile) -> UserProfile: + raise NotImplementedError + class SeedAuthorizationService(Protocol): async def assign_role(self, subject: str, role: str) -> None: @@ -151,11 +154,16 @@ async def _seed_one_user( user = User.create( email=email, - password=PasswordSerrvice.hash(password), + password_hash=PasswordSerrvice.hash(password), username=username, - fullname=fullname, ) saved_user = await user_repository.save(user) + await user_repository.save_profile( + UserProfile( + user_id=saved_user.id, + display_name=fullname, + ) + ) await authorization_service.assign_role(str(saved_user.id), role) return UserSeedResult(users_created=1, roles_assigned=1) From bc69a64327b93f18d59ff266582c80d6a9bc5720 Mon Sep 17 00:00:00 2001 From: fiqri khoirul muttaqin Date: Mon, 22 Jun 2026 12:45:11 +0700 Subject: [PATCH 6/6] docs: explain normalized database seeding --- README.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d2672b5..e330559 100644 --- a/README.md +++ b/README.md @@ -428,13 +428,13 @@ make downgrade Important: migration autogeneration depends on importing all SQLAlchemy models in `alembic/env.py`, so new module models must be imported there or through a central model registry. -Seed baseline authorization data after applying migrations: +Seed baseline records after applying migrations: ```bash make seed ``` -The seeder is idempotent. It creates default authorization resources, the default `admin` and `user` roles, default permissions, role-permission links, and matching Casbin policies without duplicating existing records. +The seeder runs all changes in one transaction and is idempotent. It creates the default authorization resources, roles (`admin`, `user`, `manager`, and `viewer`), permissions, role-permission links, and matching Casbin policies without duplicating existing records. If any seed operation fails, the transaction is rolled back. To seed an initial admin user, set these environment variables before running `make seed`: @@ -445,7 +445,17 @@ SEED_ADMIN_USERNAME=admin SEED_ADMIN_FULLNAME=System Administrator ``` -If `SEED_ADMIN_EMAIL` or `SEED_ADMIN_PASSWORD` is empty, user seeding is skipped. Existing users are not modified. +For each new seeded user, the repository creates records that follow the normalized user schema: + +- `users` stores email, username, password hash, authentication provider, and status. +- `user_profiles` stores `SEED_ADMIN_FULLNAME` (or the demo account name) as `display_name`. +- `user_settings` stores the default language, timezone, theme, and notification preferences. +- `user_security` stores the default login-attempt, lockout, password, and two-factor state. +- `user_has_roles` associates the user with its seeded role, with a matching Casbin grouping policy. + +`SEED_ADMIN_FULLNAME` is retained for configuration compatibility; it does not refer to a `users.fullname` column. The normalized schema stores this value in `user_profiles.display_name`. + +If `SEED_ADMIN_EMAIL` or `SEED_ADMIN_PASSWORD` is empty, admin-user seeding is skipped. If a seeded email already exists, the seeder does not change that user's identity, password, profile, settings, security state, or roles. When `APP_ENV=development`, the seeder can also create demo users with different roles. Set a shared development password before running `make seed`: @@ -455,11 +465,11 @@ SEED_DEVELOPMENT_USERS_PASSWORD= Development demo accounts: -- `user@example.com` with the `user` role -- `manager@example.com` with the `manager` role -- `viewer@example.com` with the `viewer` role +- `user@example.com` with display name `Default User` and the `user` role +- `manager@example.com` with display name `Todo Manager` and the `manager` role +- `viewer@example.com` with display name `Todo Viewer` and the `viewer` role -These users are skipped outside development and are not updated if they already exist. +Demo users are skipped outside development and are not modified when their email already exists. ## Testing and Quality Checks