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 @@
@@ -41,41 +29,10 @@ async function handleLogout(): Promise {
}
header {
- display: flex;
width: min(100%, 76rem);
- align-items: center;
- justify-content: space-between;
margin: 0 auto;
}
-.logout-button {
- min-height: 2.75rem;
- padding: 0.7rem 1.1rem;
- border: 0;
- border-radius: 0.7rem;
- color: #fffdf7;
- background: #183a31;
- box-shadow: 0 0.55rem 1.2rem rgb(24 58 49 / 14%);
- font-size: 0.82rem;
- font-weight: 750;
- cursor: pointer;
- transition:
- background-color 160ms ease,
- transform 160ms ease,
- box-shadow 160ms ease;
-}
-
-.logout-button:hover {
- background: #285c4e;
- box-shadow: 0 0.7rem 1.4rem rgb(24 58 49 / 18%);
- transform: translateY(-1px);
-}
-
-.logout-button:focus-visible {
- outline: 3px solid rgb(86 127 112 / 34%);
- outline-offset: 0.25rem;
-}
-
section {
width: min(100%, 42rem);
margin: clamp(6rem, 18vh, 12rem) auto 0;
diff --git a/justfile b/justfile
index ecd141f..37b0450 100644
--- a/justfile
+++ b/justfile
@@ -3,53 +3,5 @@ set shell := ["bash", "-c"]
default:
@just --list
-# Full completion gate. Start the worktree stack before running it because
-# Cypress exercises the frontend and its backend wiring.
-test-all:
- @echo "==> frontend format + lint checks"
- just frontend-format-check
- just frontend-lint-check
- @echo "==> frontend type check"
- just frontend-type-check
- @echo "==> backend static analysis"
- just backend-types-check
- @echo "==> frontend production build"
- just frontend-build
- @echo "==> backend tests"
- just backend-test
- @echo "==> frontend Cypress tests"
- just frontend-cypress-run
-
-# Backend
-
-backend-test *args:
- cd backend && php artisan test {{args}}
-
-backend-types-check:
- cd backend && composer types:check
-
fresh:
cd backend && php artisan migrate:fresh --seed
-
-# Frontend
-
-frontend-format:
- cd frontend/website && npm run format
-
-frontend-format-check:
- cd frontend/website && npm run format:check
-
-frontend-lint:
- cd frontend/website && npm run lint
-
-frontend-lint-check:
- cd frontend/website && npm run lint:check
-
-frontend-type-check:
- cd frontend/website && npm run type-check
-
-frontend-build:
- cd frontend/website && npm run build-only
-
-frontend-cypress-run:
- cd frontend/website && npm run test:e2e