diff --git a/ai/backend-context.md b/ai/backend-context.md index 9214acd..8ab6a97 100644 --- a/ai/backend-context.md +++ b/ai/backend-context.md @@ -8,76 +8,57 @@ Read `ai/shared.md` first. This file covers backend-specific rules. **Location:** `backend/`. -The backend follows a domain-oriented structure. Existing areas use entities, -DTOs, repository interfaces, Eloquent implementations, use cases, and test -fakes. Extend those patterns when adding behavior to an established area. Do -not add speculative layers or interfaces that the requested behavior does not -need. +The application is still close to the Laravel starter structure. Do not +introduce a domain architecture, repository layer, service layer, or other +abstraction before the codebase and requested behavior justify it. Match +existing Laravel conventions and inspect similar code before adding a new +pattern. -The Vue application remains a separate project under `frontend/website/`. -Keep frontend source, dependencies, builds, and delivery out of the backend -unless the user explicitly asks to integrate them. The backend root route is -intentionally unclaimed; the built-in health endpoint is `/up`. +## Laravel patterns -## Code patterns - -- Inspect similar domain code before adding a new entity, DTO, repository, - use case, controller action, or fake. -- Entities own domain state and behavior and expose descriptive methods. -- DTOs are explicit data containers for creation or transfer between seams. -- Repository interfaces define domain-facing persistence operations. Keep - Eloquent details in their implementations and register bindings in the - application provider. -- Put reusable business behavior in a use case. Give a use case a request DTO - when it accepts a payload; call `execute()` directly when it has no input. -- Document use-case exceptions with `@throws` when callers are expected to - handle them. -- Keep controllers thin. Translate HTTP input to use-case input and domain - output to the response contract. -- Catch only documented, expected exceptions at the controller boundary. Let - unexpected exceptions reach Laravel's exception handler. -- Fake repositories are in-memory implementations used by unit tests. Return - a new entity instance from create and lookup methods rather than exposing a - stored reference. +- Keep controllers thin. Put reusable business behavior in an appropriately + named application or domain class once the behavior warrants extraction. +- Use dedicated request validation rather than validating substantial payloads + inline in controllers. +- Let unexpected exceptions reach Laravel's exception handler. Catch only + exceptions that can be handled meaningfully at the current boundary. - Use Eloquent relationships and query scopes consistently rather than duplicating query fragments. +- Avoid speculative interfaces and abstractions with only one trivial + implementation. +- The Vue application is a separate project under `frontend/website/`. +- Keep frontend source, dependencies, builds, and delivery out of the backend + unless the user explicitly asks to integrate them. +- The backend root route is intentionally unclaimed. The built-in health + endpoint is `/up`. -## Unit tests +## Tests -- Follow the existing organization under `tests/Unit//`. -- Use fake repositories and fake collaborators for the behavior under test. - Construct unrelated dependency entities directly instead of routing them - through additional repositories. -- Test use-case branches at the use-case seam. Do not repeat every branch in - controller or HTTP tests. -- Plain entities, value objects, use cases, middleware, and controller units - should extend `PHPUnit\Framework\TestCase` when they do not need Laravel. -- Extend `Tests\TestCase` only when a test needs Laravel's container, facades, - database, routing, or HTTP kernel. -- Reserve direct Eloquent repository feature tests for persistence mapping, - query behavior, or database constraints that cannot be proven through a - plain unit test. +- Follow the existing PHPUnit organization under `tests/Unit/` and + `tests/Feature/`. +- Prefer `PHPUnit\Framework\TestCase` when a test only exercises plain PHP. +- Extend `Tests\TestCase` only when the test needs Laravel's container, + facades, database, routing, or HTTP kernel. +- HTTP feature tests extend `Tests\TestCase`. +- Use `RefreshDatabase` when a test reads or writes database state. +- Assert behavior at the appropriate seam: + - Unit tests cover isolated business behavior and edge cases. + - Feature tests cover routing, middleware, validation, persistence, and + response shape. +- Do not duplicate every business branch through the HTTP layer when unit + coverage already proves it. Feature tests should focus on wiring and the + public contract. -## Feature tests +## Test database -- Feature tests exercise the HTTP seam: routing, middleware, real Eloquent - bindings, cookies, persistence, validation, and response shape. -- Feature tests are additive to unit tests. Prefer a happy path and the - relevant authorization guard instead of duplicating all business branches. -- Place endpoint tests under `tests/Feature//`, extend - `Tests\TestCase`, and use `RefreshDatabase` when database state is involved. - Development and runtime use PostgreSQL through the local Unix socket. - PHPUnit uses SQLite `:memory:` as configured in `phpunit.xml`, so feature - tests are self-contained and do not need the worktree stack. +- PHPUnit intentionally uses SQLite `:memory:` as configured in + `phpunit.xml`. +- Feature tests are self-contained and do not require the process-compose + PostgreSQL service. - Never point `RefreshDatabase` tests at the development PostgreSQL database. -- Keep mail on PHPUnit's `array` transport unless a test explicitly exercises - a real mail integration. -- Authentication uses a custom database-session cookie, not a Laravel guard. - `actingAs()` does not apply. Credentialed requests use - `withCredentials()` and `withUnencryptedCookie()` because API middleware - reads the raw cookie. -- Read an unencrypted response cookie with - `$response->getCookie($name, false)`. +- Keep mail set to the PHPUnit `array` transport unless a test explicitly + exercises a real mail integration. ## PHP rules @@ -99,7 +80,7 @@ intentionally unclaimed; the built-in health endpoint is `/up`. - Do not add production repository or model APIs solely to make seeding convenient. - Use existing lookup methods for cross-seeder relationships. When a group of - records only makes sense together, keep it in one seeder and retain local + records only makes sense together, keep them in one seeder and retain local references. ## Migrations @@ -118,12 +99,15 @@ intentionally unclaimed; the built-in health endpoint is `/up`. - Once a production database exists, replace this policy with additive, forward-only migrations. -## Backend workflow +## Before completing backend work -- Run the focused PHPUnit test while developing. -- Use `just backend-types-check` for Larastan and `just backend-test` for the - full PHPUnit suite during iteration. +- Run the focused test during development. +- Run the full test suite before completion: + + ```sh + direnv exec "$(git rev-parse --show-toplevel)" php artisan test + ``` + +- Run the Composer static-analysis scripts. - Fix failures caused by the change. Report unrelated baseline failures - precisely rather than expanding scope silently. -- The shared `just test-all` command is the required completion gate. Focused - backend recipes never replace it. + precisely rather than hiding them or expanding scope without authorization. diff --git a/ai/frontend-context.md b/ai/frontend-context.md index a2acb1e..213d164 100644 --- a/ai/frontend-context.md +++ b/ai/frontend-context.md @@ -4,8 +4,7 @@ Read `ai/shared.md` first. This file covers frontend-specific rules. ## Project context -**Stack:** Vue 3.5, TypeScript 6, Vite 8, Vue Router 5, Pinia 4, Zod 4, -Cypress 15, npm. +**Stack:** Vue 3.5, TypeScript 6, Vite 8, Vue Router 5, Pinia 4, npm. **Location:** `frontend/website/`. @@ -13,16 +12,18 @@ The frontend is a standalone application. Keep its source, dependencies, development server, and production build independent from the backend unless the user explicitly requests integration. -The application currently has route-level views, reusable authentication -components, a Pinia authentication store, Zod schemas, API URL handling, and -Cypress end-to-end specs. The main entry points are: +The scaffold uses: -- `src/App.vue` for the root component. -- `src/main.ts` for app creation, Pinia, the router, and global styles. -- `src/router/index.ts` for routes and authentication guards. -- `src/stores/` for Pinia stores and API boundaries. -- `src/views/` and `src/components/` for route and reusable UI. -- `@` as the `src/` alias in Vite and TypeScript. +- `src/App.vue` as the root component. +- `src/main.ts` to create the app and install the router and Pinia. +- `src/router/index.ts` for routes. +- `src/stores/` for Pinia stores. +- `@` as an alias for `src/` in both Vite and TypeScript. + +There are no views, shared components, API layer, or configured test suite +yet. Cypress is installed as a frontend development dependency, but there is +no Cypress configuration or npm test script. Do not invent an architecture +before requested behavior establishes one. ## Package management and commands @@ -35,14 +36,32 @@ Install dependencies in a fresh checkout or worktree: direnv exec "$(git rev-parse --show-toplevel)" npm install ``` -`npm run format` and `npm run lint` rewrite files. Their `format:check` and -`lint:check` counterparts are non-mutating completion checks. `npm run build` -runs type checking and the production build; generated `dist/` output is -ignored. +To start only the development server on the port assigned by the shell hook: -`process-compose` starts the frontend as part of the complete development -stack. The frontend is directly accessible on `VITE_PORT`; Caddy serves the -backend and does not proxy the frontend. +```sh +direnv exec "$(git rev-parse --show-toplevel)" \ + npm run dev -- \ + --host 127.0.0.1 \ + --port "$VITE_PORT" \ + --strictPort +``` + +`process-compose` starts the frontend as part of the full development stack. +The frontend remains directly accessible on `VITE_PORT`; Caddy does not proxy +it. + +The available validation commands are: + +```sh +direnv exec "$(git rev-parse --show-toplevel)" npm run format +direnv exec "$(git rev-parse --show-toplevel)" npm run lint +direnv exec "$(git rev-parse --show-toplevel)" npm run type-check +direnv exec "$(git rev-parse --show-toplevel)" npm run build +``` + +`npm run format` and `npm run lint` rewrite files. Review the resulting diff. +`npm run build` runs type checking and the production build. Build output +under `dist/` is generated and ignored. ## Vue conventions @@ -53,69 +72,58 @@ backend and does not proxy the frontend. `src/router/index.ts`. - Keep page, component, composable, and store responsibilities distinct. - Prefer small components with explicit props and emitted events. +- Keep component styles scoped until the project adopts a deliberate global + styling system. - Use setup-style Pinia stores named `useXxxStore`. -- Keep application-wide resets and base styles in `src/styles/main.css`. - Keep component and view styles scoped and match established Attainly visual - patterns. -- Inspect similar files before introducing a component, composable, store, - route, or data-access pattern. +- Inspect similar files before introducing a new component, composable, + store, or data-access pattern. -## TypeScript and runtime validation +## TypeScript -- Preserve strict TypeScript and `noUncheckedIndexedAccess`. -- Do not use `any`. Model unknown external values as `unknown`, then parse or - narrow them. -- Zod schemas are the source of truth for runtime payloads. Define a schema - and derive its TypeScript type with `z.infer` instead of maintaining a - parallel hand-written interface. -- Parse every backend response at the store or API boundary before placing it - in application state. -- Keep read and write schemas separate when their shapes differ. -- Validate user-submitted forms with a Zod object schema when the form has - meaningful validation rules. Surface field errors from the schema rather - than maintaining parallel regular expressions and error logic. -- Keep request and response schemas and types near the API or store boundary - that owns them. -- Keep the `@` alias aligned across Vite, TypeScript, and future test tooling. +- Preserve the strict TypeScript configuration and + `noUncheckedIndexedAccess`. +- Do not use `any`. Model unknown external values as `unknown`, then narrow or + validate them. +- Derive types from runtime schemas if the project adopts a schema library. + Do not maintain a hand-written type that can drift from its schema. +- Validate payloads at trust boundaries, especially backend responses and + user-submitted forms. +- Keep request and response types close to the API or store boundary that + owns them. +- Keep the `@` alias aligned across Vite, TypeScript, and any future test + configuration. ## State and API access - Use Pinia for shared client state. Keep component-local state in components. -- Keep requests, response parsing, and response transformation at an API or - store boundary, not in presentation components. +- Keep server requests and response transformation at an API or store + boundary, not scattered through presentation components. - Represent loading, empty, success, validation-error, and unexpected-error states explicitly. -- Do not cast unchecked JSON to an application interface. -- Send cookie-backed API requests with the established credentials behavior. +- Do not cast unchecked JSON directly to an application interface. +- Keep read and write payload types separate when their shapes differ. ## Testing -- Cypress is configured under `cypress/` and runs through `npm run test:e2e` - or `just frontend-cypress-run`. -- Prefer the cheapest test seam that proves the behavior. Cypress covers - routing, browser forms, authentication flows, request wiring, and responsive - behavior. -- Mock backend calls in frontend-focused Cypress tests. Use the worktree - backend only for a deliberately end-to-end integration scenario. -- Assert both the request contract and the rendered response behavior when a - spec intercepts an API call. -- Keep mock payloads synchronized with exported store types and Zod schemas. - Add typed builders when payloads repeat across specs, and parse builder - output through the exported schema when practical. -- Authentication requests are mockable like every other frontend boundary. - Backend persistence, cookie creation, middleware, and mail behavior belong - in PHPUnit tests. -- No Vitest unit or component suite is configured. Do not claim that coverage. - If new pure logic or component behavior cannot be proved economically with - Cypress, establish the smallest appropriate Vitest setup test-first. +Cypress is installed, but no frontend test configuration or test script +exists yet. -## Frontend workflow +- Do not invent test commands or claim frontend tests passed. +- New frontend behavior must still follow the shared test-first workflow. + Establish the smallest appropriate test setup before implementing behavior + that needs it. +- Unit tests should cover pure transformations, composables, and store logic. +- Component tests should cover rendering, events, form behavior, and + conditional UI. +- End-to-end tests should cover routing, multi-page flows, and request wiring. +- Prefer the cheapest layer that proves the behavior. +- Mock backend requests in frontend tests. Do not retest backend persistence, + validation, authentication, or mail behavior through the frontend. -- Run the focused Cypress spec while developing when browser behavior changes. -- Run `npm run format` and `npm run lint` before committing frontend changes, - then review every rewrite. -- Use the focused `just frontend-*` recipes for development feedback. -- The shared `just test-all` command is the required completion gate. Focused - frontend checks never replace it. -- Do not claim a green gate when a command fails. Report baseline or - environmental failures precisely. +## Before completing frontend work + +- Run the focused test while developing once test tooling exists. +- Run the formatter, linter, type checker, production build, and every + configured test script affected by the change. +- Do not claim a green gate when a command fails. Report a baseline or + environmental failure precisely. diff --git a/ai/shared.md b/ai/shared.md index bf52fe5..00a1273 100644 --- a/ai/shared.md +++ b/ai/shared.md @@ -39,24 +39,6 @@ Use judgment for changes that cannot meaningfully be test-driven, such as documentation-only edits or declarative environment configuration. Validate those changes with the most relevant parser, formatter, dry run, or check. -## Maintaining these instructions - -- Treat user steering as durable when it corrects or establishes a reusable - project rule for workflow, architecture, conventions, validation, safety, - or scope. -- When durable steering is received during work, update the appropriate - `ai/*.md` file in the active worktree before completing the task. Do not - wait for a separate request to maintain the instructions. -- Put repository-wide rules in `shared.md` and stack-specific rules in the - matching backend or frontend guide. -- Merge new guidance into the existing rule set. Keep it concise, remove - duplication, and resolve conflicts between shared and stack-specific text. -- Do not persist task-specific scope, temporary directions, secrets, - environment incidents, or instructions that conflict with higher-priority - guidance. -- Commit a durable instruction update separately from implementation unless - the task itself is solely an instruction change. - ## Approval discipline - Treat dependency installation, tests, static analysis, formatting, linting, @@ -82,42 +64,58 @@ those changes with the most relevant parser, formatter, dry run, or check. - A worktree owns its own isolated stack. The flake shell hook assigns a deterministic port offset and creates worktree-local PostgreSQL state. - Start a worktree stack only when runtime or integration validation requires - it. Start it once, reuse it throughout validation, and stop it once when - finished. -- PHPUnit, frontend formatting, linting, type checking, and production builds - do not require services. The Cypress completion suite does require the - worktree stack. -- When worktree stack control is necessary, operate it directly. Do not ask - the user to start or stop worktree services. -- Never use `process-compose -t=false` for a detached stack. It can leave an + it. Start it once from the worktree root, reuse it throughout validation, + and stop it once when finished. +- Do not start any service for PHPUnit, frontend formatting, linting, type + checking, or production builds. +- When authorized worktree stack control is necessary, operate it directly. + Do not ask the user to start or stop worktree services. +- For non-interactive use, start the stack detached and stop it when finished, + as shown below. +- Do not use `process-compose -t=false` for a detached stack. It can leave an orphaned PostgreSQL process holding the data directory. - Non-interactive agent shells do not automatically load direnv. Bare project commands can use missing tools, default ports, or paths from the main checkout. -- Run PHP, Composer, Artisan, npm, tests, builds, database clients, and - services through the development environment. -- Resolve the direnv target from the worktree containing the current working - directory. Never target the main checkout or a different worktree: +- Run project tooling that depends on the repository development environment + through direnv. This includes PHP, Composer, Artisan, npm, tests, builds, + database clients, and services. +- Resolve the direnv target from the worktree containing the agent's current + working directory. Never target the main checkout or a different worktree: ```sh direnv exec "$(git rev-parse --show-toplevel)" ``` -- Git and environment-neutral read-only inspection do not need direnv. -- Start and stop a worktree stack from its root: +- Git and environment-neutral read-only file inspection do not need the + direnv wrapper. +- Worktree stack examples: ```sh direnv exec "$(git rev-parse --show-toplevel)" process-compose up -D direnv exec "$(git rev-parse --show-toplevel)" process-compose down ``` -- Run backend commands from `backend/` and frontend commands from - `frontend/website/`. The direnv target remains the worktree root. +- Run backend commands from `backend/`, or explicitly change into it in the + command. The direnv target remains the worktree root. +- Run frontend commands from `frontend/website/`. +- `process-compose` starts the frontend on the worktree's assigned port. To + start only the frontend: + + ```sh + direnv exec "$(git rev-parse --show-toplevel)" \ + npm run dev -- \ + --host 127.0.0.1 \ + --port "$VITE_PORT" \ + --strictPort + ``` + - When a normally valid check fails because a required service is down, - surface the environmental failure. Do not skip the check, change databases, - or claim the work is complete. + surface the environmental failure. Do not skip the check or silently switch + to a different database or service. - PHPUnit is self-contained and uses in-memory SQLite. It does not require the - PostgreSQL stack unless a future test explicitly targets real PostgreSQL. + PostgreSQL stack unless a future test is explicitly designed as a real + PostgreSQL integration test. ## Code style @@ -157,13 +155,14 @@ those changes with the most relevant parser, formatter, dry run, or check. - Do not include drive-by formatter or linter changes. Restore them or land them as a separate formatting commit. - If a check fails on untouched code, do not bundle an unrelated fix. Report - the baseline failure or handle it as its own explicitly scoped change. + the pre-existing failure or handle it as its own explicitly scoped change. ## Branching - Never implement features directly in the main checkout or on `master`/`main`. -- Use a dedicated worktree under `/.worktrees/`. +- Use a dedicated worktree under + `/.worktrees/`. - Create it with: ```sh @@ -184,10 +183,10 @@ those changes with the most relevant parser, formatter, dry run, or check. ``` - Never symlink `backend/vendor` or `frontend/website/node_modules` from - another checkout. Dependencies and generated files must remain + another checkout. Dependency paths and generated files must remain worktree-local. -- The shell hook installs backend dependencies but not frontend dependencies. - Install frontend dependencies from `frontend/website/`: +- The shell hook installs backend dependencies but does not install frontend + dependencies. From `frontend/website/`, provision them with: ```sh direnv exec "$(git rev-parse --show-toplevel)" npm install @@ -197,35 +196,59 @@ Do not push anything. Make commits as the TDD workflow requires. ## Before completing a change -A change is not complete until the worktree stack is ready and the unified -gate passes against that worktree: +Run the smallest relevant checks while iterating, then run every repository +gate affected by the change. -1. Start the stack detached from the worktree root: +### Backend - ```sh - direnv exec "$(git rev-parse --show-toplevel)" \ - process-compose up -D - ``` +- Run tests from `backend/`: -2. Poll `process-compose process list` until every process is running and - ready. -3. Run the complete gate from the worktree root: + ```sh + direnv exec "$(git rev-parse --show-toplevel)" php artisan test + ``` - ```sh - direnv exec "$(git rev-parse --show-toplevel)" just test-all - ``` +- Run the Composer checks defined by `backend/composer.json`: -4. Do not hand-assemble a substitute from focused commands. `test-all` runs - frontend format and lint checks, frontend type checking, Larastan, the - production build, PHPUnit, and Cypress in fail-fast order. -5. Everything must pass before completion. Report exact baseline or - environmental failures rather than hiding them. -6. If the stack was started only for validation, stop it when finished: + ```sh + direnv exec "$(git rev-parse --show-toplevel)" composer types:check + direnv exec "$(git rev-parse --show-toplevel)" composer test + ``` - ```sh - direnv exec "$(git rev-parse --show-toplevel)" process-compose down - ``` +- Do not claim a green gate when a command fails. If the failure predates the + change, report the precise baseline failure. -Focused `just` recipes are for iteration only. For Nix or shell-hook changes, -also run `nix fmt` and `nix flake check`. For service configuration changes, -also run `process-compose --dry-run`. +### Frontend + +- Run these commands from `frontend/website/`: + + ```sh + direnv exec "$(git rev-parse --show-toplevel)" npm run format + direnv exec "$(git rev-parse --show-toplevel)" npm run lint + direnv exec "$(git rev-parse --show-toplevel)" npm run type-check + direnv exec "$(git rev-parse --show-toplevel)" npm run build + ``` + +- The formatter and linters rewrite files. Review their changes before + committing. +- No frontend test runner is configured yet. Do not claim unit, component, or + end-to-end test coverage until the relevant scripts exist and pass. + +### Environment and integration + +- For Nix or shell-hook changes, run: + + ```sh + direnv exec "$(git rev-parse --show-toplevel)" nix fmt + direnv exec "$(git rev-parse --show-toplevel)" nix flake check + ``` + +- For service configuration changes, run: + + ```sh + direnv exec "$(git rev-parse --show-toplevel)" \ + process-compose --dry-run + ``` + +- When a change affects runtime wiring, start the worktree's stack and verify + the relevant endpoint or service against that worktree. +- If you started the stack only for validation, stop it before finishing. diff --git a/backend/app/Auth/UseCases/Logout/Logout.php b/backend/app/Auth/UseCases/Logout/Logout.php deleted file mode 100644 index 3cede1f..0000000 --- a/backend/app/Auth/UseCases/Logout/Logout.php +++ /dev/null @@ -1,17 +0,0 @@ -sessionRepository->deleteByToken($token); - } -} diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php index 4fdaea2..a9c1960 100644 --- a/backend/app/Http/Controllers/AuthController.php +++ b/backend/app/Http/Controllers/AuthController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers; use App\Auth\UseCases\AuthenticateUser\AuthenticateUser; use App\Auth\UseCases\AuthenticateUser\AuthenticateUserRequest; use App\Auth\UseCases\CreateSession\CreateSession; -use App\Auth\UseCases\Logout\Logout; use App\Exceptions\BadRequestException; use App\Exceptions\UnauthorizedException; use App\Http\Middleware\AuthMiddleware; @@ -20,7 +19,6 @@ class AuthController extends Controller public function __construct( private AuthenticateUser $authenticateUser, private CreateSession $createSession, - private Logout $logout, ) {} public function login(Request $request): JsonResponse @@ -73,28 +71,6 @@ class AuthController extends Controller ]); } - public function logout(Request $request): JsonResponse - { - $token = $request->cookie(AuthMiddleware::COOKIE_NAME); - if (is_string($token) && $token !== '') { - $this->logout->execute($token); - } - - $response = new JsonResponse(null, 204); - - return $response->withCookie(Cookie::create( - name: AuthMiddleware::COOKIE_NAME, - value: '', - expire: 1, - path: '/', - domain: null, - secure: false, - httpOnly: true, - raw: false, - sameSite: Cookie::SAMESITE_LAX, - )); - } - /** * @return array{id: int, email: string} */ diff --git a/backend/routes/api.php b/backend/routes/api.php index 7d7d7c1..d2e63d8 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -8,5 +8,4 @@ Route::post('/login', [AuthController::class, 'login']); Route::middleware(AuthMiddleware::class)->group(function (): void { Route::get('/me', [AuthController::class, 'me']); - Route::post('/logout', [AuthController::class, 'logout']); }); diff --git a/backend/tests/Feature/Auth/LogoutEndpointTest.php b/backend/tests/Feature/Auth/LogoutEndpointTest.php deleted file mode 100644 index 3986cb9..0000000 --- a/backend/tests/Feature/Auth/LogoutEndpointTest.php +++ /dev/null @@ -1,58 +0,0 @@ -create(new CreateUserDto( - email: new EmailAddress('user@example.com'), - passwordHash: 'hashed-password', - )); - app(SessionRepository::class)->create(new CreateSessionDto( - token: 'session-token', - user: $user, - createdAt: $now, - expiresAt: $now->modify('+7 days'), - )); - - $response = $this->withCredentials() - ->withUnencryptedCookie( - AuthMiddleware::COOKIE_NAME, - 'session-token', - )->postJson('/api/logout'); - - $response->assertNoContent(); - $response->assertCookieExpired(AuthMiddleware::COOKIE_NAME); - $this->assertNull( - app(SessionRepository::class)->findByToken('session-token'), - ); - } - - public function test_logout_rejects_a_request_without_a_cookie(): void - { - $response = $this->postJson('/api/logout'); - - $response - ->assertStatus(401) - ->assertExactJson(['error' => 'unauthenticated']); - } -} diff --git a/backend/tests/Unit/Auth/UseCases/LogoutTest.php b/backend/tests/Unit/Auth/UseCases/LogoutTest.php deleted file mode 100644 index 5accde6..0000000 --- a/backend/tests/Unit/Auth/UseCases/LogoutTest.php +++ /dev/null @@ -1,58 +0,0 @@ -sessionRepository = new FakeSessionRepository; - $this->useCase = new Logout($this->sessionRepository); - } - - public function test_existing_token_session_is_removed(): void - { - $now = new DateTimeImmutable( - '2026-07-31T12:00:00', - new DateTimeZone('UTC'), - ); - $this->sessionRepository->create(new CreateSessionDto( - token: 'session-token', - user: new User( - id: 7, - email: new EmailAddress('user@example.com'), - passwordHash: 'hashed-password', - ), - createdAt: $now, - expiresAt: $now->modify('+7 days'), - )); - - $this->useCase->execute('session-token'); - - $this->assertNull( - $this->sessionRepository->findByToken('session-token'), - ); - } - - public function test_unknown_token_does_not_throw(): void - { - $this->useCase->execute('unknown-token'); - - $this->assertNull( - $this->sessionRepository->findByToken('unknown-token'), - ); - } -} diff --git a/backend/tests/Unit/Http/Controllers/AuthControllerTest.php b/backend/tests/Unit/Http/Controllers/AuthControllerTest.php index 5372272..cded75c 100644 --- a/backend/tests/Unit/Http/Controllers/AuthControllerTest.php +++ b/backend/tests/Unit/Http/Controllers/AuthControllerTest.php @@ -4,7 +4,6 @@ namespace Tests\Unit\Http\Controllers; use App\Auth\UseCases\AuthenticateUser\AuthenticateUser; use App\Auth\UseCases\CreateSession\CreateSession; -use App\Auth\UseCases\Logout\Logout; use App\Http\Controllers\AuthController; use App\Http\Middleware\AuthMiddleware; use App\Shared\ValueObject\EmailAddress; @@ -46,11 +45,9 @@ class AuthControllerTest extends TestCase new DateTimeZone('UTC'), )), ); - $logout = new Logout($this->sessionRepository); $this->controller = new AuthController( $authenticateUser, $createSession, - $logout, ); } @@ -120,37 +117,6 @@ class AuthControllerTest extends TestCase ); } - public function test_logout_deletes_session_and_clears_cookie(): void - { - $this->createUser('correct-password'); - $this->controller->login(new Request([ - 'email' => 'user@example.com', - 'password' => 'correct-password', - ])); - $request = new Request; - $request->cookies->set( - AuthMiddleware::COOKIE_NAME, - 'session-token', - ); - - $response = $this->controller->logout($request); - - $this->assertSame(204, $response->getStatusCode()); - $this->assertNull( - $this->sessionRepository->findByToken('session-token'), - ); - $cookies = $response->headers->getCookies(); - $this->assertCount(1, $cookies); - $this->assertSame( - AuthMiddleware::COOKIE_NAME, - $cookies[0]->getName(), - ); - $this->assertSame('', $cookies[0]->getValue()); - $this->assertSame(1, $cookies[0]->getExpiresTime()); - $this->assertTrue($cookies[0]->isHttpOnly()); - $this->assertSame('lax', $cookies[0]->getSameSite()); - } - private function createUser(string $password): void { $this->userRepository->create(new CreateUserDto( diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts index cd35dfd..d7fb723 100644 --- a/frontend/website/cypress/e2e/session-auth.cy.ts +++ b/frontend/website/cypress/e2e/session-auth.cy.ts @@ -3,40 +3,6 @@ const authenticatedUser = { email: 'user@example.com', } -function interceptLogoutFlow(): void { - let authenticated = true - - cy.intercept('GET', '**/api/me', (request) => { - if (authenticated) { - request.alias = 'me' - request.reply({ - statusCode: 200, - body: { user: authenticatedUser }, - }) - return - } - - request.alias = 'loggedOutMe' - request.reply({ - statusCode: 401, - body: { error: 'unauthenticated' }, - }) - }) - cy.intercept('POST', '**/api/logout', (request) => { - expect(request.headers.accept).to.equal('application/json') - authenticated = false - request.reply({ statusCode: 204 }) - }).as('logout') -} - -function visitDashboardAndLogout(): void { - cy.visit('/dashboard') - cy.wait('@me') - cy.contains('button', 'Log out').click() - cy.wait('@logout') - cy.wait('@loggedOutMe') -} - describe('session authentication', () => { it('restores an authenticated session on a protected route', () => { cy.intercept('GET', '**/api/me', { @@ -76,18 +42,6 @@ describe('session authentication', () => { cy.location('pathname').should('equal', '/dashboard') }) - it('redirects a restored session away from the home route', () => { - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { user: authenticatedUser }, - }).as('me') - - cy.visit('/') - cy.wait('@me') - - cy.location('pathname').should('equal', '/dashboard') - }) - it('rejects a malformed authenticated-user response', () => { cy.intercept('GET', '**/api/me', { statusCode: 200, @@ -99,23 +53,4 @@ describe('session authentication', () => { cy.location('pathname').should('equal', '/login') }) - - it('logs out and redirects to login', () => { - interceptLogoutFlow() - - visitDashboardAndLogout() - - cy.location('pathname').should('equal', '/login') - }) - - it('keeps protected routes inaccessible after logout', () => { - interceptLogoutFlow() - visitDashboardAndLogout() - - cy.visit('/dashboard') - cy.wait('@loggedOutMe') - - cy.location('pathname').should('equal', '/login') - cy.location('search').should('include', 'redirect=/dashboard') - }) }) diff --git a/frontend/website/package.json b/frontend/website/package.json index 75587b0..e70648e 100644 --- a/frontend/website/package.json +++ b/frontend/website/package.json @@ -10,14 +10,10 @@ "build-only": "vite build", "type-check": "vue-tsc --build", "test:e2e": "cypress run", - "lint": "run-s lint:oxlint lint:eslint", + "lint": "run-s \"lint:*\"", "lint:oxlint": "oxlint . --fix", "lint:eslint": "eslint . --fix --cache", - "lint:check": "run-s lint:oxlint:check lint:eslint:check", - "lint:oxlint:check": "oxlint .", - "lint:eslint:check": "eslint . --cache", - "format": "oxfmt src/", - "format:check": "oxfmt --check src/" + "format": "oxfmt src/" }, "dependencies": { "pinia": "^4.0.2", diff --git a/frontend/website/src/router/index.ts b/frontend/website/src/router/index.ts index 66d053a..51e7184 100644 --- a/frontend/website/src/router/index.ts +++ b/frontend/website/src/router/index.ts @@ -9,9 +9,6 @@ const router = createRouter({ path: '/', name: 'home', component: () => import('@/views/HomeView.vue'), - meta: { - guestOnly: true, - }, }, { path: '/login', diff --git a/frontend/website/src/stores/auth.ts b/frontend/website/src/stores/auth.ts index fc9eef3..d720f18 100644 --- a/frontend/website/src/stores/auth.ts +++ b/frontend/website/src/stores/auth.ts @@ -101,20 +101,6 @@ export const useAuthStore = defineStore('auth', () => { } } - async function logout(): Promise { - try { - await fetch(`${API_BASE}/api/logout`, { - method: 'POST', - credentials: 'include', - headers: { - Accept: 'application/json', - }, - }) - } finally { - user.value = null - } - } - return { user, loading, @@ -122,6 +108,5 @@ export const useAuthStore = defineStore('auth', () => { isAuthenticated, fetchMe, login, - logout, } }) diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index c61783f..90b1b1d 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -1,23 +1,11 @@