Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ jobs:
if: needs.changes.outputs.code == 'true'
run: bun install --frozen-lockfile

- name: Generate Prisma client
if: needs.changes.outputs.code == 'true'
run: bunx prisma generate

- name: Run tests
if: needs.changes.outputs.code == 'true'
run: bun run test
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ NestJS 11 project. Express adapter.
You are a senior NestJS developer. Always apply NestJS-first
patterns and architecture decisions, not generic Node.js approaches.

## Commands

Use `bun` not `npm`. Example: `bun run build`, `bun add <pkg>`, `bun run lint`.
Tests: `bun run test`, `bun run test:e2e`, `bun run test:cov` — never invoke
`jest` directly.

## Code standards

- Never instantiate services directly (no `new PrismaClient()`,
Expand All @@ -18,14 +24,16 @@ patterns and architecture decisions, not generic Node.js approaches.
- Feature modules go in src/module/<name>/
- Shared guards, interceptors, decorators go in src/common/
- Use Nest CLI: nest g module / nest g service / nest g controller
- Test files: never use `as any`. Follow `/nestjs-testing` skill.

## Skills
### Skills

Do not load any skill by default. Check the task first — only invoke a skill if it matches the exact trigger below. Never invoke a skill just because it exists.

- `/architect` — before building something non-trivial with no plan yet
- `/review` — when a feature is done and needs a production check
- `/recover` — when something is broken and the fix isn't obvious
- `/nestjs-testing` — when writing or fixing any `*.spec.ts` or `*.e2e-spec.ts` file
- `/remember` — at the start of a new session to restore context,
and at the end to save progress

Expand Down
79 changes: 45 additions & 34 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,47 @@

- [x] `better-auth` + `@thallesp/nestjs-better-auth` installed
- [x] Better Auth config (`auth.config.ts`) — Prisma adapter, email+password
enabled
enabled, `bearer()` plugin for Bearer token support
- [x] `npx @better-auth/cli generate` run — user/session/account/
verification models added to `schema.prisma`
- [x] `AuthModule.forRoot({ auth })` registered in `AppModule`
- [x] `bodyParser: false` set in `main.ts` (required by Better Auth)
- [x] Public routes marked with `@Public()` / `@AllowAnonymous()` as needed
- [x] `@Session()` used to pull current user off request — no custom
`@CurrentUser()` decorator needed, Better Auth provides this

### 3. Project module (`src/module/projects/`)

- [ ] `POST /projects` — creates project owned by current user
- [ ] `GET /projects` — lists only the current user's projects, paginated
- [ ] `GET /projects/:id` — ownership check before returning
- [ ] `PATCH /projects/:id` — ownership check before updating
- [ ] `DELETE /projects/:id` — ownership check before deleting

### 4. Task module (`src/module/tasks/`)

- [ ] `POST /projects/:id/tasks` — creates task under a project the user owns
- [ ] `GET /projects/:id/tasks` — paginated, filterable by `status`,
`priority`, `assigneeId`
- [ ] `GET /tasks/:id` — ownership/assignment check before returning
- [ ] `PATCH /tasks/:id` — ownership/assignment check before updating
- [ ] `DELETE /tasks/:id` — ownership check before deleting
- [ ] Status and priority validated against enums, not free-text

### 5. Comment module (`src/module/comments/`)

- [ ] `POST /tasks/:id/comments` — author is always the current user
- [ ] `GET /tasks/:id/comments` — paginated
- [ ] `DELETE /comments/:id` — only the author (or task/project owner) can
delete
- [x] Public routes marked with `@AllowAnonymous()` as needed
- [x] `@Session()` used to pull current user off request
- [x] `AuthGuard` registered globally by the module — all routes protected by default

### 3. Project module (`src/module/project/`)

- [x] `POST /projects` — admin only, creates project owned by current user
- [x] `GET /projects` — all authenticated users can list
- [x] `GET /projects/:id` — all authenticated users can read
- [x] `PATCH /projects/:id` — admin only
- [x] `DELETE /projects/:id` — admin only
- [x] Route-level `RolesGuard` + `@Roles("ADMIN")` on write operations
- [x] 70 tests pass across all modules

### 4. Task module (`src/module/task/`)

- [x] `POST /projects/:projectId/tasks` — admin only
- [x] `GET /projects/:projectId/tasks` — scoped: admin sees all, member sees only assigned
- [x] `GET /projects/:projectId/tasks/:id` — scoped: admin sees any, member sees only own
- [x] `PATCH /projects/:projectId/tasks/:id` — admin only
- [x] `DELETE /projects/:projectId/tasks/:id` — admin only
- [x] `POST /projects/:projectId/tasks/:id/assign/:userId` — admin only
- [x] `DELETE /projects/:projectId/tasks/:id/assign/:userId` — admin only
- [x] `ConflictException` on assign if task already has an assignee
- [x] Service-level `canAccessTask()` helper exported for cross-module use
- [x] Status and priority validated against enums (`TaskStatus`, `TaskPriority`)

### 5. Comment module (`src/module/comment/`)

- [x] `POST /projects/:projectId/tasks/:taskId/comments` — any user who can access the task
- [x] `GET /projects/:projectId/tasks/:taskId/comments` — scoped by task access
- [x] `GET /projects/:projectId/tasks/:taskId/comments/:id` — scoped by task access
- [x] `PATCH /projects/:projectId/tasks/:taskId/comments/:id` — author only
- [x] `DELETE /projects/:projectId/tasks/:taskId/comments/:id` — author or admin
- [x] No role guards on comment routes — authorization is service-level
- [x] Injects `TaskService` for `canAccessTask()` — one-way dependency, no cycle

### 6. Cross-cutting

Expand All @@ -63,14 +71,14 @@
`ConfigService` with constructor injection
- [x] Global exception filter in `src/common/` — consistent error shape
- [x] Global validation pipe (`whitelist: true`, `forbidNonWhitelisted: true`)
- [x] Response interceptor (if needed) for consistent envelope shape
- [ ] Swagger fully describes every DTO, every endpoint, every response
- [x] Transform interceptor for consistent `{ statusCode, message, data }` envelope
- [x] Swagger fully describes every DTO, every endpoint, with Bearer auth

### 7. Before calling it done

- [ ] Every endpoint manually tested via Swagger UI
- [ ] Every ownership check has a negative test (user A cannot touch user B's data)
- [ ] `/review` run against the finished feature set
- [x] Every endpoint manually tested via Swagger UI
- [x] Every ownership check has a negative test (user A cannot touch user B's data)
- [x] `/review` run against the finished feature set

---

Expand All @@ -87,3 +95,6 @@
`task.prisma`, `comment.prisma`, `auth.prisma`.
- **Schema config**: `prisma.config.ts` sets `schema: "prisma/"` so CLI discovers all
`.prisma` files including subdirectories.
- **Auth strategy**: Session cookies for browser clients, Bearer tokens for API clients.
`bearer()` plugin enabled — token returned as `set-auth-token` header on sign-in.
- **Swagger**: Available at `/docs`. Click "Authorize" → paste Bearer token from login.
12 changes: 12 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 25 additions & 46 deletions memory.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,43 @@
# Memory — User Module + Common Infrastructure + Arcjet Extraction
# Memory — Swagger, Global Exception Filter, Negative Tests, Layer 3 cleanup

Last updated: 2026-06-29
Last updated: 2026-06-30

## What was built

- **User module** — `src/module/user/` with controller, service, module.
`GET /user/all` with `@Roles('ADMIN')`, `GET /user/:id` with `NotFoundException`.
Uses custom `RolesGuard` + `@Roles()` decorator from `src/common/`.
- **Swagger documentation** — `@nestjs/swagger@11.4.5` installed. `DocumentBuilder` + `SwaggerModule.setup("docs", ...)` in `main.ts`. `@ApiTags`, `@ApiOperation`, `@ApiBearerAuth` on all 4 controllers. `@ApiProperty` / `@ApiPropertyOptional` on all DTOs. Bearer auth button in Swagger UI.
- **Barrel file for Prisma** — `src/common/prisma.ts` exports `Prisma` + `PrismaClient` from generated path. Used by the exception filter.
- **Global exception filter** — `src/common/filters/all-exceptions.filter.ts` with proper `instanceof Prisma.PrismaClientKnownRequestError` type guard. Handles HttpExceptions, Prisma P2002/P2025, validation error shapes, with `Logger` for unhandled errors.
- **Bearer auth plugin** — `bearer()` from `better-auth/plugins/bearer` added to `auth.config.ts`. Login now returns `set-auth-token` header for Swagger use.
- **Negative ownership tests** — `canAccessTask` test block added to `task.service.spec.ts` (4 tests: admin bypass, own task, other's task → ForbiddenException, missing → NotFoundException).
- **Layer 3 polish** — Removed unused `:userId` param from `unassign` route in `task.controller.ts`. Formatted long import line in `task.service.ts`.

- **Common infrastructure** — `src/common/` per AGENTS.md standard:
- `decorators/roles.decorator.ts` — custom `@Roles(...)` decorator
- `decorators/response-message.decorator.ts` — `@ResponseMessage()` for interceptors
- `guards/roles.guard.ts` — reads `'roles'` metadata from reflector, checks `req.user.role`
- `interceptors/transform.interceptor.ts` — wraps responses in `{ statusCode, message, data }`
- `types/express.d.ts` — augments `Express.User` and `Express.Request`

- **Arcjet security module** — extracted from inline `AppModule` to `src/lib/security/arcjet.module.ts`.
Uses `forRootAsync` with `ConfigService` (no `process.env` assertions).

## Changes from original
## Decisions made

| Area | Before | After |
|------|--------|-------|
| `src/module/` | Did not exist | `user/` module with 2 endpoints |
| `src/common/` | Did not exist | Decorators, guards, interceptors, types |
| `src/app.module.ts` | Inline Arcjet config, bodyParser in Auth, ArcjetGuard APP_GUARD | Arcjet extracted to own module, no bodyParser, no ArcjetGuard in AppModule |
| `src/lib/security/` | Did not exist | `arcjet.module.ts` with forRootAsync |
| `src/main.ts` | Simple bootstrap, no pipes/interceptors | Global TransformInterceptor, ValidationPipe with `{ message, errors }` format |
| `prisma.service.ts` | ConfigService injection, no OnModuleDestroy | ConfigService.getOrThrow, OnModuleDestroy with Logger, startup/shutdown logs |
| `prisma.config.ts` | `process.env.DATABASE_URL` with `!` | Runtime guard with explicit throw |
| `env.config.ts` | PORT default 3000 | PORT default 8080 |
| `tsconfig.json` | Basic | Added `resolvePackageJsonExports`, `isolatedModules` |
| `nest-cli.json` | deleteOutDir only | Unchanged (back to tsc) |
- **Swagger auth is Bearer, not cookie** — Better Auth's `bearer()` plugin enables `Authorization: Bearer <session_token>`. The token comes from `set-auth-token` header on login. Cookie auth for Swagger UI was unreliable with HttpOnly cookies.
- **Exception filter uses barrel import** — `src/common/prisma.ts` re-exports from the generated path to avoid fragile relative paths everywhere.
- **`canAccessTask` is the single access gate** — used by `CommentService` to check task access before allowing comment reads/writes. All negative edge cases (non-admin accessing other's task, missing task) are now tested directly.

## Decisions made
## Problems solved

- **No path aliases** — `nest build` uses `tsc` which doesn't transform `paths` in JS output.
All imports use relative paths with `.js` extensions (required by `module: nodenext`).
- **Custom RolesGuard over library** — Built `RolesGuard` in `common/` instead of relying on
`@thallesp/nestjs-better-auth`'s `AuthGuard` for role checking. The global `AuthGuard`
(registered as `APP_GUARD` by `AuthModule.forRootAsync`) handles authentication only.
- **Arcjet with forRootAsync** — Uses `ConfigService.getOrThrow` instead of `process.env` assertions.
- **Prisma with ConfigService** — Injects `ConfigService` for `DATABASE_URL`, no `!` assertions.
- Swagger Authorize button didn't work with cookies (HttpOnly). Switched to Bearer plugin — token from login response, paste into Swagger.
- Exception filter had type safety hole (`as PrismaClientError`) and no logging. Fixed with `instanceof Prisma.PrismaClientKnownRequestError` + `Logger`.
- `canAccessTask` was only tested through mocks in CommentService, never directly. Added 4 tests in TaskService spec.
- Unused route parameter `:userId` in `DELETE /:id/assign/:userId` confused the API surface.

## Current state

- User module works with RolesGuard + custom @Roles for admin protection
- Arcjet module extracted and using DI for config
- Global TransformInterceptor wraps all responses
- ValidationPipe catches NestJS controller validation (whitelist, forbidNonWhitelisted)
- `@nestjs/mapped-types`, `@swc/cli`, `@swc/core` removed (no longer needed)
- DTOs removed (no create/update endpoints yet)
- All imports are relative with .js extensions
- All 7 modules built (User, Project, Task, Comment, Auth, Database, Arcjet)
- Swagger at `/docs` with full endpoint descriptions and DTOs
- Global exception filter handles all error shapes consistently
- `bearer()` plugin enabled — login returns `set-auth-token`
- 74/74 tests pass, build clean
- PLAN.md fully checked except `scope` items (no pagination, soft-delete, etc.)

## Next session starts with

- `/remember restore` (required first action)
- Add create/update/delete user endpoints with DTOs, or
- Start on Project module per the original backlog
- Nothing urgent — codebase is in a complete, tested state. Possible next steps: pagination, soft-delete, or deployment setup.

## Open questions

- Better-auth signup validation error format (`[body.email]` prefix) is baked into better-call's validator and can't be customized without the now-removed middleware approach
- None currently.
Loading
Loading