candle-annotator/openspec/changes/user-accounts/specs/user-auth/spec.md
Marko Djordjevic c36ab7c146 Implement task 6.1: Create PUT /api/auth/profile endpoint for updating user display name
- Create src/app/api/auth/profile/route.ts with PUT handler
- Validates user is authenticated (returns 401 if not)
- Validates request body has a non-empty name field
- Updates user's name in the database
- Returns 200 with updated user data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:20:20 +01:00

6.9 KiB

ADDED Requirements

Requirement: Auth.js v5 configuration

The system SHALL configure Auth.js v5 (next-auth@5) in src/auth.ts with JWT session strategy, Credentials provider, and Google OAuth provider. The configuration SHALL export handlers, auth, signIn, and signOut functions.

Scenario: Auth config exports

  • WHEN src/auth.ts is imported
  • THEN it exports handlers (GET/POST), auth (session getter), signIn, and signOut functions

Scenario: JWT session strategy

  • WHEN a user authenticates successfully
  • THEN a JWT token is issued containing user.id, user.email, and user.name
  • AND no database session record is created

Scenario: JWT callbacks embed user ID

  • WHEN the JWT callback fires after sign-in
  • THEN the token.id is set to the user's UUID from the database
  • AND the session callback maps token.id to session.user.id

Requirement: Credentials provider with email/password

The Credentials provider SHALL accept email and password fields. The authorize function SHALL look up the user by email in the users table, verify the password against the stored password_hash using bcryptjs, and return the user object on success or null on failure.

Scenario: Valid credentials

  • WHEN a user submits correct email and password
  • THEN the authorize function returns the user object with id, email, name
  • AND a JWT session is created

Scenario: Invalid password

  • WHEN a user submits correct email but wrong password
  • THEN the authorize function returns null
  • AND no session is created

Scenario: Non-existent email

  • WHEN a user submits an email that does not exist in the database
  • THEN the authorize function returns null

Scenario: OAuth-only user attempts password login

  • WHEN a user who registered via Google (no password_hash) submits their email with any password
  • THEN the authorize function returns null

Requirement: Google OAuth provider

The Google OAuth provider SHALL be configured with AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET environment variables. On first Google sign-in, the system SHALL create a new user record with provider: 'google' and provider_account_id set to the Google sub ID. On subsequent sign-ins, the system SHALL find the existing user by email.

Scenario: First Google sign-in creates user

  • WHEN a user signs in with Google for the first time
  • THEN a new user record is created with provider: 'google', password_hash: null, provider_account_id set to the Google sub ID, and name/email/image from the Google profile

Scenario: Returning Google sign-in

  • WHEN a user signs in with Google and their email already exists in the database
  • THEN the existing user is returned and no duplicate is created

Scenario: Missing Google credentials

  • WHEN AUTH_GOOGLE_ID or AUTH_GOOGLE_SECRET environment variables are not set
  • THEN the Google provider SHALL not be included in the providers list and email/password login remains available

Requirement: Auth API route handler

The system SHALL create src/app/api/auth/[...nextauth]/route.ts that exports the GET and POST handlers from src/auth.ts.

Scenario: Auth routes respond

  • WHEN a request is made to /api/auth/signin, /api/auth/signout, /api/auth/session, or /api/auth/callback/google
  • THEN the NextAuth handler processes the request

Requirement: Registration API endpoint

The system SHALL provide a POST /api/auth/register endpoint that creates a new user account. The endpoint SHALL accept { name, email, password } in the request body. The password SHALL be hashed with bcryptjs (salt rounds: 10) before storage. The email SHALL be checked for uniqueness.

Scenario: Successful registration

  • WHEN POST /api/auth/register is called with valid name, email, and password (min 8 characters)
  • THEN a new user is created with hashed password, provider: 'credentials'
  • AND the response is HTTP 201 with { id, email, name }

Scenario: Duplicate email

  • WHEN POST /api/auth/register is called with an email that already exists
  • THEN the response is HTTP 409 with { error: "Email already registered" }

Scenario: Invalid password length

  • WHEN POST /api/auth/register is called with a password shorter than 8 characters
  • THEN the response is HTTP 400 with { error: "Password must be at least 8 characters" }

Scenario: Missing required fields

  • WHEN POST /api/auth/register is called without email or password
  • THEN the response is HTTP 400 with { error: "Email and password are required" }

Requirement: Auth proxy for route protection

The system SHALL create a proxy.ts file at the project root that uses Auth.js auth() to check JWT sessions. The proxy SHALL enforce authentication on protected routes.

Scenario: Unauthenticated access to /app

  • WHEN an unauthenticated user requests any path under /app or /app/*
  • THEN the proxy redirects to /login

Scenario: Unauthenticated access to /api

  • WHEN an unauthenticated user requests any path under /api/* except /api/auth/* and /api/health
  • THEN the proxy returns HTTP 401

Scenario: Authenticated access to /login

  • WHEN an authenticated user requests /login or /register
  • THEN the proxy redirects to /app

Scenario: Public routes pass through

  • WHEN any user requests /, /login, /register, /api/auth/*, or /api/health
  • THEN the proxy allows the request to proceed without auth check

Requirement: Auth helper for API routes

The system SHALL provide a getAuthUser() helper function in src/lib/auth.ts that extracts the authenticated user from the current session. All protected API routes SHALL call this function.

Scenario: Authenticated request

  • WHEN getAuthUser() is called in an API route with a valid JWT session
  • THEN it returns { id, email, name } from the session

Scenario: Unauthenticated request

  • WHEN getAuthUser() is called in an API route without a valid session
  • THEN it returns null

Requirement: SessionProvider wrapper

The system SHALL wrap the app layout with next-auth/react SessionProvider so client components can use the useSession() hook to access auth state.

Scenario: Session available in client components

  • WHEN a client component calls useSession() inside the protected app layout
  • THEN it receives { data: session, status: "authenticated" } with session.user.id, session.user.email, session.user.name

Requirement: npm dependencies for auth

The project SHALL add next-auth@5 (Auth.js v5), bcryptjs, and @types/bcryptjs to the dependencies.

Scenario: Dependencies installed

  • WHEN package.json is inspected
  • THEN next-auth, bcryptjs are in dependencies and @types/bcryptjs is in devDependencies