feat(auth): implement local admin setup, login, and auth UI (#30)#56
Conversation
Add backend authentication endpoints (GET /api/auth/me, POST /api/auth/setup, POST /api/auth/login) with argon2 password hashing and timing-attack prevention. Create frontend SetupPage and LoginPage with client-side validation, accessible forms, and error handling. Add auth API client module and update routing. Backend: - userService with createLocalUser, verifyPassword, findByEmail, countUsers - Auth routes with JSON schema validation (email format, password min 12 chars) - Config: SESSION_DURATION and SECURE_COOKIES environment variables - argon2@0.43.0 for OWASP-recommended password hashing Frontend: - SetupPage: centered card form for initial admin account creation - LoginPage: centered card form for email/password login - authApi.ts: typed API client for auth endpoints - Routes /setup and /login outside AppShell (no sidebar) - Webpack extensionAlias for ESM .js -> .ts resolution Tests: 65 new tests (userService unit + auth routes integration) All quality gates pass, 281 tests total across 22 suites. Fixes #30 Co-Authored-By: Claude backend-developer (Sonnet 4.5) <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer (Sonnet 4.5) <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
steilerDev
left a comment
There was a problem hiding this comment.
[product-owner] PR #56 Review for Story #30: Local Admin Authentication -- Initial Setup & Login
Acceptance Criteria Verification
| # | Criterion | Verdict | Evidence |
|---|---|---|---|
| 1 | POST /api/auth/setup creates first admin user (email, displayName, password) |
PASS | auth.ts line 64-89: endpoint creates user with role admin, auth_provider local. Returns 201 with user object. Integration test creates first admin user successfully (201) confirms. |
| 2 | Setup returns 403 SETUP_COMPLETE when users already exist |
PASS | auth.ts line 72-74: checks countUsers > 0 before creation. Error code SETUP_COMPLETE. Integration test returns 403 SETUP_COMPLETE when users already exist confirms. |
| 3 | Password stored as argon2 hash (never plain text) | PASS | userService.ts line 48: argon2.hash(password). Unit test creates user with hashed password (not plain text) verifies hash starts with $argon2id$. Response never includes passwordHash (tested in both setup and login response tests). |
| 4 | POST /api/auth/login authenticates with email/password |
PASS | auth.ts line 99-134: finds user by email, verifies password via argon2.verify(), returns 200 with user profile. Integration test succeeds with valid credentials (200) confirms. |
| 5 | Login returns 401 INVALID_CREDENTIALS for wrong credentials (generic message) |
PASS | auth.ts line 116 and 127: both non-existent user and wrong password return same error code and message Invalid email or password. Tests for wrong password and non-existent email both verify INVALID_CREDENTIALS with identical message. |
| 6 | Login returns 401 ACCOUNT_DEACTIVATED for deactivated users |
PASS | auth.ts line 120-122: checks deactivatedAt before password verification, throws ACCOUNT_DEACTIVATED. Integration test fails for deactivated user (401 ACCOUNT_DEACTIVATED) confirms. |
| 7 | GET /api/auth/me returns setupRequired:true when no users, false otherwise |
PASS | auth.ts lines 38-55: returns 200 with setupRequired: true/false based on user count. Two integration tests verify both states. |
| 8 | Timing-attack prevention (constant-time comparison for non-existent users) | PASS | auth.ts lines 110-115: when user not found or has no passwordHash, a dummy argon2 hash is verified before returning error. This ensures consistent response time whether the email exists or not. Tests verify this for both non-existent users and OIDC users without passwords. |
| 9 | Password minimum 12 characters enforced | PASS | auth.ts line 13: JSON schema validation minLength: 12. SetupPage client-side validation also enforces 12 chars. Integration tests verify boundary cases: 12 chars accepted, 11 chars rejected. |
| 10 | Setup page with form validation | PASS | SetupPage.tsx: form with email, displayName, password, confirmPassword fields. Client-side validation for required fields, password min length, password confirmation match. Error display with role="alert" and aria-invalid attributes. Accessible labels with htmlFor. |
| 11 | Login page with form validation and error display | PASS | LoginPage.tsx: form with email and password fields. Client-side validation for required fields. API error display via errorBanner with role="alert". Accessible labels, aria-invalid, aria-describedby for error association. |
Result: 11/11 acceptance criteria PASS.
UAT Scenario Coverage
| # | Scenario | Status |
|---|---|---|
| 1 | Setup endpoint available when no users exist | Covered by AC #7 test |
| 2 | First admin creation via setup | Covered by AC #1 test |
| 3 | Setup endpoint locked after first admin | Covered by AC #2 test |
| 4 | Setup page shown when no users exist | Manual (frontend page exists at /setup) |
| 5 | Password hashing with secure algorithm | Covered by AC #3 test |
| 6 | Invalid email format rejected | Covered by integration test |
| 7 | Password too short rejected | Covered by AC #9 tests |
| 8 | Empty display name rejected | Covered by integration test |
| 9 | Successful login with valid credentials | Covered by AC #4 test |
| 10 | Login page shown for returning users | Manual (frontend page exists at /login) |
| 11 | Invalid email returns generic error | Covered by AC #5 test |
| 12 | Invalid password returns generic error | Covered by AC #5 test |
| 13 | Deactivated user cannot log in | Covered by AC #6 test |
| 14-15 | Session persistence across reload | Deferred to Story #32 (noted in code comments) |
| 16-17 | Timing attack mitigation | Covered by AC #8 tests |
Agent Responsibilities Check
| Responsibility | Agent | Status |
|---|---|---|
| Backend implementation | backend-developer |
DONE (Co-Authored-By in commit) |
| Frontend implementation | frontend-developer |
DONE (Co-Authored-By in commit) |
| Tests (65 new, 281 total) | qa-integration-tester |
DONE (Co-Authored-By in commit) |
| UAT scenarios | uat-validator |
DONE (posted on issue #30) |
| Architecture sign-off | product-architect |
PENDING -- no review on PR yet |
| Security review | security-engineer |
PENDING -- no review on PR yet |
Quality Gates
- CI (Quality Gates): PASS
- CI (Docker): PASS
- Dependency pinning:
argon2pinned at exact0.43.0-- PASS - Accessibility: Focus styles on inputs and buttons,
role="alert"on errors,aria-invalid/aria-describedby-- PASS - Conventions: Conventional commit,
Fixes #30, proper agent attribution -- PASS - Scope discipline: No scope creep detected. Session creation correctly deferred to Story #32 with
// NOTE:comments.
Non-Blocking Observations
-
Deactivated user error code reveals account state: The
ACCOUNT_DEACTIVATEDerror code and message "Account has been deactivated" discloses that the account exists and is deactivated. The AC explicitly requires this code (AC #6), so this is correct per requirements. However, the UAT Scenario 13 expected a "generic error message (no indication that account is deactivated)". The security-engineer should evaluate whether this information leakage is acceptable in the security review, and if not, a follow-up story should be created to align the ACs. Not blocking for this PR since the implementation matches the story's acceptance criteria. -
additionalProperties: falsebehavior: The JSON schemas setadditionalProperties: falsebut Fastify/AJV defaults strip extra properties silently rather than rejecting them. The tests correctly assert 201/200 for payloads with extra fields. This is acceptable behavior but worth documenting if stricter validation is desired later. -
Login page
successMessageplaceholder text: The login success message includes "Session management will be implemented in Story #32." -- this is a developer-facing note that should be removed when Story #32 is implemented. Acceptable as a temporary placeholder.
Decision
CONDITIONALLY APPROVED -- All 11 acceptance criteria pass, 65 tests cover the UAT scenarios comprehensively, CI is green, dependency is properly pinned, and test authorship is verified. The PR correctly scopes session management to Story #32.
Approval is conditional on:
product-architectmust review and approve (architecture compliance, code quality)security-engineermust review and approve (especially the timing-attack prevention, argon2 configuration, and the deactivated-user error code information leakage noted above)
Both reviews are required per CLAUDE.md before merge.
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect] Architecture review of PR #56 — Story #30: Local Admin Authentication
Verdict: APPROVE (with observations for refinement)
CI is green (Quality Gates + Docker pass). The implementation is well-structured, follows established patterns, and correctly implements the API contract for the three endpoints in scope. The observations below are minor and appropriate for the refinement phase.
Architecture Compliance
Passes. The implementation follows the established Fastify plugin pattern and aligns with the plugin registration order documented in the Architecture wiki:
- Auth routes registered after
dbplugin (correct, needsfastify.db) - Uses
{ prefix: '/api/auth' }for route mounting (correct convention) - Route handlers throw
AppErrorinstances, never construct error responses directly (correct per ADR-009) - Service layer (
userService.ts) is properly separated from route handlers (good separation of concerns)
The registration order in app.ts is: config -> errorHandler -> compress -> db -> authRoutes -> health -> static. This is consistent with the Architecture wiki's documented order (auth routes go between db and static).
API Contract Alignment
Passes. All three endpoints match the API Contract wiki specification:
| Endpoint | Contract | Implementation | Match |
|---|---|---|---|
GET /api/auth/me |
200 OK, {user, setupRequired, oidcEnabled} |
Correct | Yes |
POST /api/auth/setup |
201 Created, {user}, 403 SETUP_COMPLETE |
Correct | Yes |
POST /api/auth/login |
200 OK, {user}, 401 INVALID_CREDENTIALS/ACCOUNT_DEACTIVATED |
Correct | Yes |
Validation schemas match the contract:
- Setup: email (format: email), displayName (1-100 chars), password (min 12)
- Login: email (string), password (string)
- Both use
additionalProperties: false(good; stricter than the test comments suggest about "additionalProperties ignored" — note: Fastify may strip extra properties rather than rejecting, depending on AJVremoveAdditionalsetting)
UserResponse shape correctly excludes passwordHash and oidcSubject as specified. The toUserResponse() function uses explicit field picking (not spread/delete), which is the secure pattern for sensitive field exclusion.
Schema Alignment
Passes. userService.ts operates on the Drizzle users schema from server/src/db/schema.ts, which matches the Wiki Schema page and migration 0001_create_users_and_sessions.sql. Column names (snake_case DB, camelCase TS) follow conventions.
Shared Types Usage
Passes. UserResponse imported from @cornerstone/shared — the returned shape from toUserResponse() matches the interface. ErrorCode is properly typed through AppError constructor usage (e.g., 'SETUP_COMPLETE', 'INVALID_CREDENTIALS', 'ACCOUNT_DEACTIVATED').
Security Review Items
-
Timing attack prevention (GOOD): Login route hashes a dummy password when user is not found or has no
passwordHash. This prevents email enumeration via response time differences. -
Generic error messages (GOOD):
INVALID_CREDENTIALSalways returns "Invalid email or password" regardless of whether the email exists or password was wrong. Matches the security requirement in the API Contract. -
Deactivation check ordering: In
auth.tslines ~96-101, the deactivation check happens before password verification. This means a deactivated user getsACCOUNT_DEACTIVATEDwithout their password being checked. This is functionally correct per the API Contract (deactivated users get a distinct error), but note it does reveal that the email exists and the account was deactivated. The API Contract explicitly specifies this distinction, so this is intentional. -
Sensitive field exclusion (GOOD):
toUserResponse()uses positive field selection (explicitly lists safe fields), not negative selection (delete sensitive fields). This is the correct pattern — new sensitive columns added later will NOT accidentally leak.
Code Quality
- ESM imports: All
.jsextensions present in import paths (correct for ESM Node.js). - Naming conventions: Files use camelCase (
userService.ts,authApi.ts), React components use PascalCase (SetupPage.tsx,LoginPage.tsx). Matches convention. - Type safety: No unjustified
anytypes in production code. Test files useas anywitheslint-disablecomments only for verifying that sensitive fields are absent — acceptable. request.body as { ... }type assertion: Used in route handlers. This is standard Fastify practice since the JSON schema validation guarantees the shape at runtime.
Test Coverage
65 new tests (31 userService unit + 34 auth route integration) covering:
- All happy paths for all 3 endpoints
- Validation boundary cases (password min length 11 vs 12, empty displayName, missing fields)
- Error cases (SETUP_COMPLETE, INVALID_CREDENTIALS, ACCOUNT_DEACTIVATED)
- Sensitive field exclusion verification
- Timing attack prevention verification
- Case-sensitivity for email lookup
- OIDC user attempting local login (returns INVALID_CREDENTIALS)
The coverage appears thorough for the 95% target. QA agent will verify exact coverage numbers.
Observations for Refinement
These are non-blocking items appropriate for the epic refinement phase:
-
additionalProperties: falsetest description mismatch: Tests at lines ~1289-1305 and ~1532-1550 are named "accepts payload even with additional properties (additionalProperties ignored)" but the schema actually setsadditionalProperties: false. Fastify withadditionalProperties: falsemay strip or ignore extra properties rather than rejecting them (depends on AJV configuration). The test passes, but the comment/name is misleading — consider updating the test name to reflect actual behavior. -
Config test updates are minimal: The 4 existing config tests now include
sessionDurationandsecureCookiesin expected output. However, there are no new tests for invalidSESSION_DURATION(e.g., non-numeric, negative) or invalidSECURE_COOKIES(e.g.,"yes","1"). The validation logic is present inconfig.tsbut untested. QA should add coverage. -
Client-side
autoCompleteattribute: LoginPage hasautoComplete="email"andautoComplete="current-password"(good for password managers). SetupPage is missingautoCompleteattributes on its inputs — minor UX improvement for the refinement pass. -
Hardcoded dummy hash:
auth.tsline ~1836 contains a hardcoded argon2 hash string for timing attack prevention. This works correctly but could be made more maintainable by generating the dummy hash once at module load time (e.g.,const DUMMY_HASH = await argon2.hash('dummy-password-for-timing');). Not a security issue, just a readability improvement. -
setupSchemaandloginSchemaare route-local: These JSON schemas are defined at the top ofauth.ts. As the number of routes grows, consider extracting schemas to a shared location (e.g.,server/src/schemas/). Not needed now, but worth noting for future organization. -
CSS duplication between SetupPage and LoginPage: The two CSS Module files are nearly identical (~290 lines combined with ~95% overlap). Consider extracting a shared
AuthPage.module.cssduring refinement. -
Session not created on setup/login: Both endpoints correctly note
// NOTE: Session creation will be added in Story #32. The success messages in the frontend components also reference Story #32. This is properly scoped and well-documented.
Summary: This is a solid implementation that faithfully follows the API Contract, Schema, and Architecture documentation. The error handling pattern, sensitive field exclusion, and timing attack prevention are all correctly implemented. All observations above are minor refinement items. Approve.
steilerDev
left a comment
There was a problem hiding this comment.
[security-engineer]
Security Review: PR #56 (Story #30 - Local Admin Authentication)
I have completed a comprehensive security review of PR #56. This PR implements the initial setup and login endpoints with local password authentication using Argon2.
Summary
Overall Assessment: APPROVED with recommendations for future stories
This PR implements secure password authentication with proper hashing, timing attack prevention, and sensitive data protection. No critical or high-severity vulnerabilities were identified. All security controls appropriate for this phase (initial setup + login without session management) are correctly implemented.
Findings
✅ Password Hashing (SECURE)
Status: No issues found
The implementation uses argon2@0.43.0 with secure default parameters:
- Algorithm: argon2id (OWASP-recommended hybrid mode)
- Memory cost: 65536 KiB (64 MiB) — exceeds OWASP minimum of 15 MiB
- Time cost: 3 iterations — meets OWASP minimum of 2
- Parallelism: 4 threads — exceeds OWASP minimum of 1
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/services/userService.ts:48—await argon2.hash(password)uses secure defaults/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/services/userService.test.ts:165— test confirms argon2id formatnpm audit— 0 vulnerabilities, argon2@0.43.0 has no known CVEs
✅ Timing Attack Prevention (SECURE)
Status: No issues found
The login endpoint implements proper timing attack prevention by always hashing a dummy password when the user is not found or has no password hash (OIDC users).
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.ts:110-116— Dummy password verification when user not found or passwordHash is null:if (!user || !user.passwordHash) { // Hash a dummy password to prevent timing attacks await userService.verifyPassword( '$argon2id$v=19$m=65536,t=3,p=4$aGVsbG93b3JsZA$cZn5d+rFz8E4HMhH+3e6Ug', password, ); throw new AppError('INVALID_CREDENTIALS', 401, 'Invalid email or password'); }
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.test.ts:614-668— Tests confirm timing attack prevention for both non-existent users and OIDC users
This prevents attackers from using response time differences to enumerate valid email addresses.
✅ Input Validation (SECURE)
Status: No issues found
JSON schema validation is properly implemented with appropriate constraints:
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.ts:6-17— Setup schema:email: { type: 'string', format: 'email' }— validates RFC 5322 email format via AJVdisplayName: { type: 'string', minLength: 1, maxLength: 100 }— prevents empty and excessively long namespassword: { type: 'string', minLength: 12 }— enforces strong password minimum lengthadditionalProperties: false— rejects unexpected fields (NOTE: Fastify default AJV ignores this, see informational finding below)
- Client-side validation mirrors server-side constraints in
/Users/franksteiler/Documents/Sandboxes/cornerstone/client/src/pages/SetupPage/SetupPage.tsx:23-48
No SQL injection risk — all database queries use Drizzle ORM's parameterized queries.
✅ Sensitive Data Protection (SECURE)
Status: No issues found
Password hashes and OIDC subjects are correctly excluded from all API responses.
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/services/userService.ts:17-28—toUserResponse()function explicitly excludespasswordHashandoidcSubject- All auth routes (
/api/auth/setup,/api/auth/login,/api/auth/me) usetoUserResponse()before returning user data /Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.test.ts:323-365— Tests confirm passwordHash and oidcSubject are never in API responses
TypeScript type safety ensures developers cannot accidentally leak sensitive fields — the UserResponse type excludes these fields at compile time.
✅ Error Message Security (SECURE)
Status: No issues found
Error messages follow secure patterns:
- Login failures return generic
INVALID_CREDENTIALSfor both "user not found" and "wrong password" (prevents account enumeration) ACCOUNT_DEACTIVATEDreveals account state, but this is acceptable UX — users need to know why they can't loginSETUP_COMPLETEreveals setup state, but this is acceptable — prevents duplicate admin creation attempts
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.ts:116— Generic error for non-existent users/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.ts:127— Generic error for wrong password/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.test.ts:463-477— Test confirms same error code for both cases
✅ Frontend Security (SECURE)
Status: No issues found
Client code follows secure practices:
- No XSS vectors: No use of
dangerouslySetInnerHTML,innerHTML, oreval()in any client code - Proper form handling: All user input is handled via controlled React components
- Accessibility: ARIA attributes (
aria-invalid,aria-describedby,role="alert") used correctly - Password hygiene: Passwords are cleared from state after submission (lines 66-70 in SetupPage.tsx, lines 51-53 in LoginPage.tsx)
- Autocomplete: Proper
autoCompleteattributes (email,current-password) for password manager compatibility
Evidence:
- Grep search for XSS patterns returned no matches
/Users/franksteiler/Documents/Sandboxes/cornerstone/client/src/pages/LoginPage/LoginPage.tsx:97,119— Proper autocomplete attributes
✅ Dependency Security (SECURE)
Status: No issues found
All dependencies are secure:
npm audit— 0 vulnerabilities (0 fixable)argon2@0.43.0— No known CVEs- All other dependencies inherited from previous PRs (already audited)
📋 Informational: Fastify additionalProperties Behavior
Severity: Informational
OWASP Category: A04 - Insecure Design (minor configuration note)
The JSON schema specifies additionalProperties: false, but Fastify's default AJV configuration does not reject extra properties — they are silently ignored.
Evidence:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.test.ts:305-321— Test confirms extra fields are ignored, not rejected
Why this is acceptable:
- Extra fields are ignored, not processed — no security impact
- Fail-open for unknown fields is a reasonable design choice
- This is Fastify's documented default behavior
- For a small self-hosted application (1-5 users), strict OpenAPI validation is not critical
If stricter validation is desired in the future:
Configure Fastify with AJV removeAdditional: true or removeAdditional: 'all' in app.ts:
const app = Fastify({
ajv: {
removeAdditional: true, // Strips extra properties
// OR: removeAdditional: 'all' // Strips ALL extra properties including nested
},
});Risk if unaddressed: None. This is a design choice, not a vulnerability.
Recommendations for Future Stories
The following security controls are missing from this PR but are expected to be addressed in Story #32 (Session Management) or later stories:
1. Rate Limiting (MEDIUM Priority)
Add rate limiting to /api/auth/login and /api/auth/setup endpoints to prevent brute-force attacks and credential stuffing.
Recommendation: Use @fastify/rate-limit with appropriate thresholds:
import rateLimit from '@fastify/rate-limit';
await app.register(rateLimit, {
max: 5, // 5 requests
timeWindow: '15 minutes', // per 15 minutes
skipOnError: false,
});2. CSRF Protection (HIGH Priority for Session Management)
When session cookies are implemented in Story #32, ensure they use SameSite=Strict or SameSite=Lax to prevent CSRF attacks.
Recommendation: Set cookie flags in session configuration:
reply.setCookie('session', sessionToken, {
httpOnly: true,
secure: fastify.config.secureCookies,
sameSite: 'strict', // or 'lax' if external OAuth redirect needed
maxAge: fastify.config.sessionDuration,
});3. Security Headers (MEDIUM Priority)
Add @fastify/helmet to set security headers (CSP, HSTS, X-Frame-Options, etc.).
Recommendation:
npm install @fastify/helmetThen in app.ts:
import helmet from '@fastify/helmet';
await app.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // CSS Modules may need unsafe-inline
imgSrc: ["'self'", 'data:'],
},
},
});4. Account Lockout Mechanism (LOW Priority)
Consider adding temporary account lockout after N failed login attempts to further prevent credential stuffing.
Conclusion
This PR implements secure authentication with no critical or high-severity vulnerabilities. Password hashing uses OWASP-recommended Argon2id with secure parameters, timing attack prevention is correctly implemented, sensitive data is properly excluded from API responses, and all input validation is appropriate.
The missing security controls (rate limiting, CSRF protection, security headers) are expected to be addressed in future stories and do not block approval of this PR.
Recommendation: APPROVED — No blocking issues. Proceed with merge after product-owner and product-architect reviews are complete.
Review Completed: 2026-02-09
Reviewed By: security-engineer agent (Claude Sonnet 4.5)
Files Reviewed:
/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/services/userService.ts/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/routes/auth.ts/Users/franksteiler/Documents/Sandboxes/cornerstone/client/src/pages/SetupPage/SetupPage.tsx/Users/franksteiler/Documents/Sandboxes/cornerstone/client/src/pages/LoginPage/LoginPage.tsx/Users/franksteiler/Documents/Sandboxes/cornerstone/client/src/lib/authApi.ts/Users/franksteiler/Documents/Sandboxes/cornerstone/server/src/plugins/config.ts- All test files for auth routes and user service
|
🎉 This PR is included in version 1.7.0-beta.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 1.7.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
GET /api/auth/me,POST /api/auth/setup,POST /api/auth/login) with argon2 password hashing and timing-attack preventionChanges
Backend
server/src/services/userService.ts— user CRUD functions (createLocalUser, verifyPassword, findByEmail, countUsers, countActiveUsers, toUserResponse)server/src/routes/auth.ts— GET /me, POST /setup, POST /login with JSON schema validationserver/src/plugins/config.ts— SESSION_DURATION and SECURE_COOKIES configserver/src/app.ts— register auth routesserver/package.json— added argon2@0.43.0.env.example— new env vars documentedFrontend
client/src/lib/authApi.ts— typed API client for auth endpointsclient/src/pages/SetupPage/— admin setup form (email, display name, password, confirm)client/src/pages/LoginPage/— login form (email, password)client/src/App.tsx— /setup and /login routes outside AppShellclient/webpack.config.cjs— extensionAlias for ESM .js → .ts resolutionTests
server/src/services/userService.test.ts— 31 unit testsserver/src/routes/auth.test.ts— 34 integration testsTest plan
Fixes #30
🤖 Generated with Claude Code