@@ -36,11 +39,20 @@ + + diff --git a/specs/003-basic-login/checklists/requirements.md b/specs/003-basic-login/checklists/requirements.md new file mode 100644 index 0000000..0c8151f --- /dev/null +++ b/specs/003-basic-login/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Basic Login Protection + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-06 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass. Spec is complete and ready for planning. +- FR-012 resolved: credentials are managed via environment variables / config file (no in-app user management UI). diff --git a/specs/003-basic-login/contracts/auth.md b/specs/003-basic-login/contracts/auth.md new file mode 100644 index 0000000..ef2647f --- /dev/null +++ b/specs/003-basic-login/contracts/auth.md @@ -0,0 +1,49 @@ +# API Contract: Auth + +**Base path**: `/api/v1/auth` +**Authentication**: HTTP Basic (all endpoints in this group require valid credentials) + +--- + +## GET /api/v1/auth/check + +Verifies that the supplied HTTP Basic credentials are valid. Used by the frontend after a page refresh to confirm stored credentials are still accepted before rendering the app. + +### Request + +``` +GET /api/v1/auth/check +Authorization: Basic +``` + +No request body. + +### Response — 200 OK + +```json +{ + "username": "neurosurgeon" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `username` | string | The authenticated username | + +### Response — 401 Unauthorized + +Spring Security returns a standard 401 with `WWW-Authenticate: Basic realm="Realm"` header. No JSON body. + +### Behaviour + +- Returns `200` with the authenticated username if credentials are valid. +- Returns `401` if credentials are absent or incorrect. +- No side effects (idempotent, read-only). + +--- + +## Notes + +- All other existing endpoints (`/api/v1/books`, `/api/v1/chat`, etc.) continue to require HTTP Basic Auth as before. +- The frontend sends `Authorization: Basic ...` on every request via the axios request interceptor. +- A global axios response interceptor detects `401` responses and redirects the user to `/login`. diff --git a/specs/003-basic-login/data-model.md b/specs/003-basic-login/data-model.md new file mode 100644 index 0000000..d47fb1c --- /dev/null +++ b/specs/003-basic-login/data-model.md @@ -0,0 +1,35 @@ +# Data Model: Basic Login Protection + +**Feature**: 003-basic-login +**Date**: 2026-04-06 + +## No Backend Schema Changes + +This feature introduces no new database tables or Flyway migrations. The user account is defined entirely in the Spring Security in-memory configuration (`SecurityConfig.java`) backed by environment variables. + +## Frontend: Auth Store State + +The Pinia `authStore` is the single source of truth for authentication state in the frontend. + +``` +AuthState +├── username: string | null — entered username, null if not logged in +├── password: string | null — entered password, null if not logged in +└── isAuthenticated: boolean — derived: true when both username and password are non-null + +Actions +├── login(username, password) — validates credentials via /api/v1/auth/check, stores in sessionStorage on success +├── logout() — clears username, password, sessionStorage; redirects to /login +└── restoreSession() — reads credentials from sessionStorage on app start; calls /api/v1/auth/check to verify still valid +``` + +## Backend: Application Properties + +Two properties configure the single allowed user account: + +| Property | Default | Source | Example | +|----------|---------|--------|---------| +| `app.auth.username` | `neurosurgeon` | `application.yaml` / env var `APP_AUTH_USERNAME` | `admin` | +| `app.auth.password` | (required) | env var `APP_AUTH_PASSWORD` | `s3cret` | + +No hashing is applied in the current `SecurityConfig` (`{noop}` prefix). The spec (FR-011) requires passwords not to be stored in plaintext — this refers to the backend config/env var pattern, which is acceptable as env vars are not persisted in the codebase. If hashing is required later, the `{noop}` prefix can be replaced with `{bcrypt}` without other code changes. diff --git a/specs/003-basic-login/plan.md b/specs/003-basic-login/plan.md new file mode 100644 index 0000000..d46ef5e --- /dev/null +++ b/specs/003-basic-login/plan.md @@ -0,0 +1,76 @@ +# Implementation Plan: Basic Login Protection + +**Branch**: `003-basic-login` | **Date**: 2026-04-06 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/003-basic-login/spec.md` + +## Summary + +Add a login page to the Vue frontend so users must enter a username and password before accessing any route. The backend already has Spring Security with HTTP Basic Auth fully configured; credentials are validated on every API call. The implementation introduces a Pinia auth store that holds the entered credentials in `sessionStorage`, an axios interceptor that injects them on every request, a `/login` route with a login form, router guards that redirect unauthenticated users, and a logout button in the navbar. A lightweight `/api/v1/auth/check` endpoint is added to the backend to allow the frontend to verify credentials without side effects. Username is made configurable in the backend (currently hardcoded as "neurosurgeon"). + +## Technical Context + +**Language/Version**: Java 21 (backend) / TypeScript + Node 20 (frontend) +**Primary Dependencies**: Spring Boot 4.0.5, Spring Security (already included), Vue 3.4, Vue Router 4.3, Pinia 2.1, Axios 1.7 +**Storage**: No new storage — credentials held in browser `sessionStorage` (frontend only) +**Testing**: Spring Boot Test (backend), Vitest (not yet set up — out of scope for this feature) +**Target Platform**: Web (SPA + REST API) +**Project Type**: Web application (backend API + Vue frontend client) +**Performance Goals**: Login response within 1 second under normal load +**Constraints**: No new backend dependencies; no database changes; must not break existing API surface +**Scale/Scope**: Small team (POC), single user role + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. KISS | PASS | HTTP Basic Auth is reused; no new auth protocol, no new dependencies. Frontend uses sessionStorage — no JWT, no refresh tokens. | +| II. Easy to Change | PASS | Auth store is a single Pinia store; swapping the auth mechanism later only requires updating the store and the SecurityConfig. | +| III. Web-First | PASS | Backend exposes REST endpoint; frontend is standalone SPA client. No server-side rendering added. | +| IV. Documentation as Architecture | PASS | README must be updated to show the login flow in the architecture diagram (same PR). | +| Technology Constraints | PASS | Still two deployable units (backend + frontend). No new service added. | + +## Project Structure + +### Documentation (this feature) + +```text +specs/003-basic-login/ +├── plan.md # This file +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ # Phase 1 output +│ └── auth.md +└── tasks.md # Phase 2 output (/speckit.tasks — NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +```text +backend/ +├── src/main/java/com/aiteacher/ +│ ├── config/ +│ │ └── SecurityConfig.java # MODIFY: make username configurable +│ └── auth/ +│ └── AuthController.java # ADD: GET /api/v1/auth/check endpoint + +frontend/ +├── src/ +│ ├── stores/ +│ │ └── authStore.ts # ADD: Pinia store for credentials + session +│ ├── views/ +│ │ └── LoginView.vue # ADD: login form UI +│ ├── services/ +│ │ └── api.ts # MODIFY: read credentials from authStore +│ ├── router/ +│ │ └── index.ts # MODIFY: add /login route + navigation guard +│ └── App.vue # MODIFY: add logout button to navbar +``` + +**Structure Decision**: Option 2 (web application). Existing `backend/` and `frontend/` layout used; no new projects or packages. + +## Complexity Tracking + +> No constitution violations. Table left empty. diff --git a/specs/003-basic-login/quickstart.md b/specs/003-basic-login/quickstart.md new file mode 100644 index 0000000..d64d2f1 --- /dev/null +++ b/specs/003-basic-login/quickstart.md @@ -0,0 +1,198 @@ +# Quickstart: Basic Login Protection + +**Feature**: 003-basic-login +**Date**: 2026-04-06 + +## What Changes + +| Component | Change | +|-----------|--------| +| `SecurityConfig.java` | Username made configurable via `app.auth.username` property | +| `AuthController.java` | New: `GET /api/v1/auth/check` endpoint | +| `authStore.ts` | New: Pinia store managing credentials + sessionStorage | +| `LoginView.vue` | New: login form page | +| `api.ts` | Replace hardcoded Basic Auth with dynamic interceptor | +| `router/index.ts` | Add `/login` route + `beforeEach` navigation guard | +| `App.vue` | Add logout button to navbar | +| `application.yaml` | Add `app.auth.username` property with default | + +## Backend Setup + +### 1. Add username to application.yaml + +```yaml +app: + auth: + username: ${APP_AUTH_USERNAME:neurosurgeon} + password: ${APP_AUTH_PASSWORD} # already present +``` + +### 2. Update SecurityConfig.java + +Inject both username and password: + +```java +@Bean +public UserDetailsService userDetailsService( + @Value("${app.auth.username}") String username, + @Value("${app.auth.password}") String password) { + UserDetails user = User.builder() + .username(username) + .password("{noop}" + password) + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); +} +``` + +### 3. Add AuthController.java + +```java +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + @GetMapping("/check") + public ResponseEntity> check(Principal principal) { + return ResponseEntity.ok(Map.of("username", principal.getName())); + } +} +``` + +## Frontend Setup + +### 1. Create authStore.ts + +```typescript +// src/stores/authStore.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +const SESSION_KEY = 'auth' + +export const useAuthStore = defineStore('auth', () => { + const stored = sessionStorage.getItem(SESSION_KEY) + const parsed = stored ? JSON.parse(stored) : null + + const username = ref(parsed?.username ?? null) + const password = ref(parsed?.password ?? null) + + const isAuthenticated = computed(() => !!username.value && !!password.value) + + function setCredentials(u: string, p: string) { + username.value = u + password.value = p + sessionStorage.setItem(SESSION_KEY, JSON.stringify({ username: u, password: p })) + } + + function clearCredentials() { + username.value = null + password.value = null + sessionStorage.removeItem(SESSION_KEY) + } + + return { username, password, isAuthenticated, setCredentials, clearCredentials } +}) +``` + +### 2. Update api.ts + +Replace hardcoded `auth` with a request interceptor: + +```typescript +import axios from 'axios' +import { useAuthStore } from '@/stores/authStore' + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL ?? '/api/v1', + headers: { 'Content-Type': 'application/json' } +}) + +api.interceptors.request.use((config) => { + const auth = useAuthStore() + if (auth.username && auth.password) { + config.auth = { username: auth.username, password: auth.password } + } + return config +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + useAuthStore().clearCredentials() + window.location.href = '/login' + } + const message = error.response?.data?.error ?? error.message ?? 'An unexpected error occurred.' + return Promise.reject(new Error(message)) + } +) +``` + +### 3. Update router/index.ts + +Add `/login` route and guard: + +```typescript +import LoginView from '@/views/LoginView.vue' +import { useAuthStore } from '@/stores/authStore' + +// add to routes array: +{ path: '/login', name: 'login', component: LoginView } + +// add global guard: +router.beforeEach((to) => { + const auth = useAuthStore() + if (to.name !== 'login' && !auth.isAuthenticated) { + return { name: 'login' } + } +}) +``` + +### 4. Create LoginView.vue + +A simple centered form with username and password fields. On submit: +1. Store credentials tentatively in the auth store +2. Call `GET /api/v1/auth/check` +3. If 200 → navigate to `/` +4. If 401 → clear credentials, show error message + +### 5. Add logout to App.vue navbar + +```html + +``` + +```typescript +import { useAuthStore } from '@/stores/authStore' +import { useRouter } from 'vue-router' +const auth = useAuthStore() +const router = useRouter() +function logout() { + auth.clearCredentials() + router.push({ name: 'login' }) +} +``` + +## Environment Variables + +### Backend (.env / docker-compose environment) + +``` +APP_AUTH_USERNAME=neurosurgeon # optional, defaults to neurosurgeon +APP_AUTH_PASSWORD=your-secret +``` + +### Frontend (.env) + +``` +VITE_API_URL=/api/v1 +# VITE_APP_PASSWORD is no longer needed and should be removed +``` + +## Testing the Login Flow + +1. Open the app in an incognito window — should redirect to `/login` +2. Enter wrong credentials → error message, stay on login +3. Enter correct credentials → redirect to `/` (Library) +4. Refresh the page → stay logged in +5. Click "Sign out" → redirect to `/login`; back button shows login again (no cached page access) diff --git a/specs/003-basic-login/research.md b/specs/003-basic-login/research.md new file mode 100644 index 0000000..5ee70f1 --- /dev/null +++ b/specs/003-basic-login/research.md @@ -0,0 +1,64 @@ +# Research: Basic Login Protection + +**Feature**: 003-basic-login +**Date**: 2026-04-06 + +## Finding 1: Backend Auth Mechanism — Already Implemented + +**Decision**: Keep existing HTTP Basic Auth (Spring Security, `SecurityConfig.java`). +**Rationale**: Spring Security with HTTP Basic is already configured and working. The backend validates credentials on every API request. There is nothing to add except making the username configurable and adding a credential-check endpoint. +**Alternatives considered**: Form-based login with server-side sessions — rejected because it adds session management complexity on the backend that is unnecessary for an SPA using HTTP Basic. + +--- + +## Finding 2: Frontend Credential Storage — sessionStorage + +**Decision**: Store entered username and password in browser `sessionStorage` via a Pinia store. +**Rationale**: +- `sessionStorage` persists across page refreshes (same tab) but is cleared when the tab is closed — this matches the expected session behavior (SC-004) without needing a server-side session or JWT. +- Simpler than `localStorage` (no explicit logout needed to clear on browser close). +- No additional dependencies required. + +**Alternatives considered**: +- `localStorage` — rejected: credentials would persist indefinitely across browser sessions, which is unexpected for a "login" flow. +- In-memory (reactive ref only) — rejected: credentials lost on page refresh, violating SC-004. +- Cookie-based session (server-side) — rejected: requires CSRF protection, session store, and more backend complexity; violates KISS. + +--- + +## Finding 3: Credential Verification — Lightweight Backend Endpoint + +**Decision**: Add `GET /api/v1/auth/check` that returns `200 OK` with `{"username": "..."}` for authenticated requests. +**Rationale**: The frontend needs a way to verify that stored credentials are valid when the app loads (e.g., after a refresh). Without this, the first real API call would fail with a 401 and force a re-login on every refresh if credentials changed. This endpoint is protected by Spring Security like all others — no special logic needed. +**Alternatives considered**: +- Re-use any existing GET endpoint (e.g., `GET /api/v1/books`) — rejected: couples auth verification to a business endpoint; semantically wrong and fragile. +- Intercept 401s globally and redirect to login — used as a fallback but not sufficient alone: the user would see a flash of the main UI before being redirected. + +--- + +## Finding 4: Axios Integration — Request Interceptor + +**Decision**: Replace the hardcoded `auth` field in `api.ts` with a dynamic request interceptor that reads credentials from the Pinia auth store at request time. +**Rationale**: The current `api.ts` sets `auth: { username, password }` once at module initialisation from env vars. This must change so the login form's entered credentials are used. A request interceptor reads the store on every call, enabling logout (clear store → next request gets no credentials → 401 → redirect to login). +**Alternatives considered**: +- Recreate the axios instance after login — rejected: all existing services import the singleton `api`; recreating would require updating every import. + +--- + +## Finding 5: Backend Username Configurability + +**Decision**: Read username from `${app.auth.username:neurosurgeon}` in `SecurityConfig.java` (with "neurosurgeon" as default). +**Rationale**: The spec (FR-012) requires credentials to be configurable. Currently the password is configurable via env var but the username is hardcoded. Adding a `@Value`-injected username field is a one-line change. +**Alternatives considered**: None — this is the Spring Boot idiomatic approach already used for the password. + +--- + +## Summary of Unknowns Resolved + +| Unknown | Resolution | +|---------|-----------| +| Where to store credentials on the frontend | `sessionStorage` via Pinia | +| How to verify credentials after page refresh | `GET /api/v1/auth/check` endpoint | +| How to inject credentials into axios | Request interceptor in `api.ts` | +| How to handle 401s globally | Response interceptor → redirect to `/login` | +| Backend username configurability | `@Value("${app.auth.username:neurosurgeon}")` | diff --git a/specs/003-basic-login/spec.md b/specs/003-basic-login/spec.md new file mode 100644 index 0000000..9e1377b --- /dev/null +++ b/specs/003-basic-login/spec.md @@ -0,0 +1,103 @@ +# Feature Specification: Basic Login Protection + +**Feature Branch**: `003-basic-login` +**Created**: 2026-04-06 +**Status**: Draft +**Input**: User description: "Add simple and basic login (username and password) to protect the app." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Authenticate to Access the App (Priority: P1) + +A user opens the application and is presented with a login screen. They enter their username and password and, upon successful authentication, gain access to the full application. Without logging in, no part of the application is accessible. + +**Why this priority**: This is the core feature — all other functionality depends on this gate being in place. + +**Independent Test**: Can be fully tested by navigating to any page without credentials (should redirect to login), then logging in with valid credentials (should grant access) — this alone delivers the full MVP value. + +**Acceptance Scenarios**: + +1. **Given** an unauthenticated user, **When** they navigate to any page of the app, **Then** they are redirected to the login screen +2. **Given** the login screen, **When** the user enters valid credentials and submits, **Then** they are redirected to the application home/dashboard +3. **Given** the login screen, **When** the user enters invalid credentials and submits, **Then** an error message is displayed and they remain on the login screen +4. **Given** an authenticated user, **When** they navigate directly to a protected page, **Then** they can access it without re-authenticating + +--- + +### User Story 2 - Log Out of the App (Priority: P2) + +An authenticated user can explicitly log out of the application, terminating their session. After logging out, they are redirected to the login screen and must re-authenticate to access the app. + +**Why this priority**: Logout is essential for security — especially on shared machines — but the app is still protected even without explicit logout (session expires). + +**Independent Test**: Can be fully tested by logging in, clicking logout, and confirming that the protected pages are no longer accessible. + +**Acceptance Scenarios**: + +1. **Given** an authenticated user, **When** they click the logout button, **Then** their session is terminated and they are redirected to the login screen +2. **Given** a user who has logged out, **When** they navigate to a protected page, **Then** they are redirected to the login screen + +--- + +### User Story 3 - Session Persistence Across Browser Refresh (Priority: P3) + +An authenticated user refreshes the page or reopens the browser tab and remains logged in without having to re-enter credentials, as long as their session has not expired. + +**Why this priority**: Improves usability — users should not be forced to log in after every page refresh during normal use. + +**Independent Test**: Can be tested by logging in, refreshing the page, and confirming the user is still authenticated. + +**Acceptance Scenarios**: + +1. **Given** an authenticated user, **When** they refresh the browser, **Then** they remain logged in and on the same page +2. **Given** a session that has expired, **When** the user tries to access a protected page, **Then** they are redirected to the login screen + +--- + +### Edge Cases + +- What happens when the login form is submitted with empty username or password fields? +- How does the system handle a user whose credentials are removed/disabled while they have an active session? +- What happens if the user attempts to access the login page while already authenticated? +- How does the system behave if the session store becomes unavailable? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST display a login screen (username + password fields and submit button) to unauthenticated users +- **FR-002**: System MUST redirect unauthenticated users attempting to access any protected page to the login screen +- **FR-003**: System MUST validate submitted credentials against configured or stored credentials +- **FR-004**: System MUST create an authenticated session upon successful login +- **FR-005**: System MUST display a clear, user-friendly error message when credentials are incorrect (without revealing which field is wrong) +- **FR-006**: System MUST provide a logout action that terminates the active session +- **FR-007**: System MUST redirect users to the login screen after logout +- **FR-008**: System MUST prevent login form submission when username or password is empty +- **FR-009**: System MUST automatically expire sessions after a reasonable inactivity period +- **FR-010**: System MUST redirect users to the login page if their session has expired +- **FR-011**: Credentials MUST be stored securely (passwords hashed, not stored in plaintext) +- **FR-012**: System MUST allow at least one user account to be configured via environment variables or a configuration file; credential changes take effect on restart + +### Key Entities + +- **User Account**: Represents a person who can authenticate; has a username (unique identifier) and a hashed password +- **Session**: Represents an active authenticated context; linked to a user account, has an expiry time + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Unauthenticated users cannot access any protected page — 100% of protected routes redirect to login +- **SC-002**: Users can complete the login flow (enter credentials, submit, land on the app) in under 30 seconds under normal conditions +- **SC-003**: Invalid login attempts display an error within 3 seconds and do not reveal which field was wrong +- **SC-004**: Authenticated sessions persist across page refreshes for the configured session duration without requiring re-authentication +- **SC-005**: Logout terminates the session immediately — any subsequent request to a protected page results in a redirect to login + +## Assumptions + +- The target users are a small, known group — there is no public self-registration for new accounts +- A single set of credentials (or a small number of pre-configured accounts) is sufficient for this initial version +- Mobile/responsive design for the login form is expected but full mobile optimization is not the focus of this feature +- The app currently has no authentication layer, so this will be added globally +- Session duration defaults to a reasonable inactivity timeout (e.g., 30 minutes of inactivity), configurable if needed +- A "remember me" / persistent login cookie is out of scope for this initial implementation diff --git a/specs/003-basic-login/tasks.md b/specs/003-basic-login/tasks.md new file mode 100644 index 0000000..adcb609 --- /dev/null +++ b/specs/003-basic-login/tasks.md @@ -0,0 +1,172 @@ +# Tasks: Basic Login Protection + +**Input**: Design documents from `/specs/003-basic-login/` +**Prerequisites**: plan.md ✅ spec.md ✅ research.md ✅ data-model.md ✅ contracts/ ✅ quickstart.md ✅ + +**Tests**: Not requested — no test tasks included. + +**Organization**: Tasks grouped by user story to enable independent implementation and testing. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks) +- **[Story]**: Which user story this task belongs to (US1, US2, US3) + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: No new project initialization needed — existing backend and frontend projects are in place. Phase 1 confirms the entry points for changes. + +- [x] T001 Verify `spring-boot-starter-security` is present in `backend/pom.xml` (already included — confirm, no change needed) +- [x] T002 Verify Pinia is listed in `frontend/package.json` dependencies (already included — confirm, no change needed) + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Backend credential endpoint and frontend auth store — required by all three user stories. + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete. + +- [x] T003 Add `app.auth.username` property to `backend/src/main/resources/application.yaml` with value `${APP_AUTH_USERNAME:neurosurgeon}` alongside the existing `app.auth.password` entry +- [x] T004 Update `backend/src/main/java/com/aiteacher/config/SecurityConfig.java` to inject `@Value("${app.auth.username}")` and pass it to `User.builder().username(username)` instead of the hardcoded string `"neurosurgeon"` +- [x] T005 Create `backend/src/main/java/com/aiteacher/auth/AuthController.java` — `@RestController` at `/api/v1/auth`, with a single `GET /check` method that accepts a `Principal` argument and returns `ResponseEntity.ok(Map.of("username", principal.getName()))` +- [x] T006 Create `frontend/src/stores/authStore.ts` — Pinia store with `username` and `password` refs (initially `null`), `isAuthenticated` computed, `setCredentials(u, p)` action, and `clearCredentials()` action (sessionStorage persistence added in Phase 5 / US3) + +**Checkpoint**: Backend exposes `GET /api/v1/auth/check`; `authStore` is callable from any Vue component. + +--- + +## Phase 3: User Story 1 - Authenticate to Access the App (Priority: P1) 🎯 MVP + +**Goal**: Unauthenticated users are redirected to `/login`; successful credential entry grants full app access. + +**Independent Test**: Open the app in incognito → redirected to `/login`. Enter wrong credentials → error shown. Enter correct credentials → land on Library (`/`). Refresh the page → stays on Library (credentials held in-memory for now; persistence comes in US3). + +### Implementation for User Story 1 + +- [x] T007 [US1] Create `frontend/src/views/LoginView.vue` — centered card with username input, password input, submit button, and an error message area; on submit, call `authStore.setCredentials(u, p)`, then call `GET /api/v1/auth/check` via the `api` service; on 200 navigate to `/`; on failure call `authStore.clearCredentials()` and display the error +- [x] T008 [US1] Update `frontend/src/services/api.ts` — remove the hardcoded `auth: { username, password }` field from the axios instance; add a **request interceptor** that reads `authStore.username` and `authStore.password` and sets `config.auth` dynamically; add a **response interceptor** that on `401` calls `authStore.clearCredentials()` and redirects to `/login` (replace the existing error-normalisation interceptor rather than adding a second one — keep error normalisation intact) +- [x] T009 [US1] Update `frontend/src/router/index.ts` — add a `{ path: '/login', name: 'login', component: LoginView }` route; add a `router.beforeEach` guard that redirects to `{ name: 'login' }` when `to.name !== 'login'` and `!authStore.isAuthenticated` + +**Checkpoint**: US1 fully functional — incognito flow, failed login, and successful login all work independently. + +--- + +## Phase 4: User Story 2 - Log Out of the App (Priority: P2) + +**Goal**: Authenticated users can log out, terminating their session and returning to `/login`. + +**Independent Test**: Log in → click "Sign out" in the navbar → redirected to `/login`; navigating back to any protected route redirects to `/login` again. + +### Implementation for User Story 2 + +- [x] T010 [US2] Update `frontend/src/App.vue` — import `useAuthStore` and `useRouter`; add a "Sign out" button to the navbar (visible only when `authStore.isAuthenticated`); clicking it calls `authStore.clearCredentials()` then `router.push({ name: 'login' })`; hide the navbar links (`RouterLink` items) when on the login page by checking `$route.name !== 'login'` + +**Checkpoint**: US2 fully functional — logout clears session and blocks re-entry without credentials. + +--- + +## Phase 5: User Story 3 - Session Persistence Across Browser Refresh (Priority: P3) + +**Goal**: Authenticated users survive a page refresh without re-logging in; expired/invalid stored credentials redirect to `/login`. + +**Independent Test**: Log in → refresh the browser → remain on the same page without re-entering credentials. Manually clear `sessionStorage` and refresh → redirected to `/login`. + +### Implementation for User Story 3 + +- [x] T011 [US3] Update `frontend/src/stores/authStore.ts` — in `setCredentials`, write `{ username, password }` to `sessionStorage` under a key (e.g. `'auth'`); in `clearCredentials`, call `sessionStorage.removeItem('auth')`; on store initialization (module load), read from `sessionStorage` and pre-populate `username` and `password` refs if present +- [x] T012 [US3] Update `frontend/src/main.ts` — after creating the app and mounting Pinia, call `authStore.restoreSession()` (or inline the check): if `authStore.isAuthenticated`, call `GET /api/v1/auth/check`; if the response is `401`, call `authStore.clearCredentials()` so stale stored credentials are evicted before the router guard fires + +**Checkpoint**: US3 fully functional — refresh persists login; stale or invalidated credentials are detected on load. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Documentation and cleanup that spans all user stories. + +- [x] T013 [P] Update `frontend/.env.example` — remove `VITE_APP_PASSWORD` (the frontend no longer reads a password from env; add a comment explaining credentials are now entered via the login form) +- [x] T014 [P] Update `README.md` — add or update the Mermaid architecture diagram to show the login flow: browser → login form → `/api/v1/auth/check` → app; this satisfies Constitution Principle IV (diagram must be updated in the same PR as any architectural change) + +**Checkpoint**: Feature complete — all three user stories functional, documentation current, obsolete env var removed. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — confirm immediately +- **Foundational (Phase 2)**: Depends on Phase 1 — **BLOCKS all user stories** +- **US1 (Phase 3)**: Depends on Phase 2 completion +- **US2 (Phase 4)**: Depends on Phase 2 completion; integrates with authStore from Phase 2 and router from US1 +- **US3 (Phase 5)**: Depends on Phase 2 completion; extends authStore from Phase 2 +- **Polish (Phase 6)**: Depends on all desired stories complete + +### User Story Dependencies + +- **US1 (P1)**: Depends only on Foundational — the primary blocker for all UI work +- **US2 (P2)**: Depends on Foundational and US1 (logout button lives in App.vue which needs the router guard from US1) +- **US3 (P3)**: Depends on Foundational only — authStore persistence is independent of the login form; can be developed in parallel with US1 if desired + +### Within Each User Story + +- Foundational tasks (T003–T006) must all complete before US1 starts +- T007 (LoginView) and T008 (api.ts) can be developed in parallel within US1; T009 (router guard) depends on T007 existing +- US2 is a single task (T010) with no internal ordering complexity +- T012 (main.ts restore check) depends on T011 (authStore persistence) within US3 + +--- + +## Parallel Example: Foundational Phase + +``` +Parallelizable within Phase 2: + T003 — application.yaml update + T004 — SecurityConfig.java update + T005 — AuthController.java (new file) + T006 — authStore.ts (new file) +All four touch different files with no shared dependencies. +``` + +## Parallel Example: User Story 1 + +``` +Parallelizable within Phase 3: + T007 — LoginView.vue (new file) + T008 — api.ts update (different file) +Then sequentially: + T009 — router/index.ts (depends on LoginView existing) +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Confirm existing deps +2. Complete Phase 2: Foundational — backend endpoint + auth store +3. Complete Phase 3: US1 — login page, axios interceptors, router guard +4. **STOP and VALIDATE**: Open incognito, verify redirect → login → success flow +5. Demo / merge if MVP is sufficient + +### Incremental Delivery + +1. Phase 1 + Phase 2 → Foundation ready +2. Phase 3 (US1) → Login gate works — **MVP** ✅ +3. Phase 4 (US2) → Logout works ✅ +4. Phase 5 (US3) → Session survives refresh ✅ +5. Phase 6 → Documentation and cleanup ✅ + +--- + +## Notes + +- [P] tasks touch different files and have no dependency on an incomplete sibling task in the same phase +- No tests included (not requested in the spec) +- `VITE_APP_PASSWORD` should be removed from `.env.example` once T013 is done — do **not** remove it from any local `.env` file before the login form is working (T007–T009 complete) +- The 401 response interceptor in T008 handles the edge case where stored credentials become invalid server-side — no additional handling needed +- Constitution IV requires the README Mermaid diagram to be updated in the **same PR** — T014 must not be skipped