title: "Scaffold the user module and its resources"
context: |
Build a `user` domain: the identity at the centre of everything — almost every
other module points an `owner` at `User`. A profile is private: a user sees and
edits only their own; an admin manages any account. Auth is owner-or-admin on
profile reads/updates; listing the full user base is admin-only. Registration
is open (anonymous visitor creates their own account); only an admin may
lock/ban/block/delete. Align the entity to `IUser` from the `@ooneex/user`
package (extends `IBase`: `isLocked`, `isBanned`, `isBlocked`, `isPublic`,
`language`, soft-delete `deletedAt`) — reuse its enums and types. If
`modules/user/` already exists, this work is void — do not run.
goal: |
Create the `user` module + needed resources, self-service profile + admin control.
## Notes
- If `modules/user/` exists, STOP and report. Else `/module:create` user, then
build each resource via its `*:create` skill (`--module=user`), respecting
controllers → services → repositories → entities, registering all.
- Model `User` against `IUser`; reuse the package's enums/string-literal types.
- Judge each resource; create the justified, skip the rest with a reason.
Defaults: entity + repository always; service + controller per use case (open
registration, read/update own profile, admin list, admin lock/ban/block/
delete); permission always (owner-or-admin on profile read/update, admin-only
list, reuse the permission service); event yes (most-subscribed lifecycle —
registered, banned — so other modules react without coupling); seed if the
project uses seeds (bootstrap admin); queue for notification emails if a queue
exists; migration only if applicable; translation/storage/workflow skip
(avatar is a reference; flows are transactional service logic).
- Never store/return secrets in the clear: hash passwords, keep them out of
every read/list response, compare server-side only.
- Enforce email uniqueness; throw a typed conflict (`UserAlreadyExistsException`)
at registration.
- Lock/ban/block are admin-only state, not deletion: such users are rejected at
auth with a typed exception; deletion is a soft delete (`deletedAt`) that
preserves the owner relations other modules hold.
- Throw typed exceptions (e.g. `UserNotFoundException`), never return null;
another user's profile must return the same not-found result.
### Data Model
- Every other module's `owner` points back here via `@ManyToOne(() => User)`;
this module owns the inverse only where it tracks the collection.
dod: |
- [ ] Aborts with a report if `modules/user/` exists
- [ ] `user` module created and registered into the app and `SharedModule`
- [ ] `User` entity aligned to `IUser` with fields: `email` (unique), `roles`,
`externalId` (nullable), `username` (nullable), `name` (nullable),
`firstName`/`lastName` (nullable), `avatar` (nullable), `bio` (nullable),
`phone` (nullable), `birthDate` (nullable), `timezone` (nullable), `lang`
(`LocaleType`), `isLocked`/`lockedAt`, `isBanned`/`bannedAt`/`banReason`,
`isBlocked`/`blockedAt`/`blockReason`, `isPublic`, `createdAt`, `updatedAt`,
`deletedAt` (nullable)
- [ ] Anonymous visitor can register; user reads/updates only their own
profile; admin reads/updates/locks/bans/blocks/deletes any; listing is
admin-only; another user's profile returns not-found
- [ ] Duplicate email rejected; passwords hashed, secrets never leave the server
- [ ] Locked/banned/blocked user rejected at authentication
- [ ] Deletion is a soft delete preserving other modules' owner relations
- [ ] Lifecycle events (registered, banned) emitted
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root