This commit is contained in:
Bastian Masanek
2025-10-30 08:24:44 +01:00
commit 6e50ec7034
73 changed files with 27355 additions and 0 deletions

903
CLAUDE.md Normal file
View File

@@ -0,0 +1,903 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**my.experimenta.science** is an e-commerce platform for the experimenta Science Center. It allows visitors to purchase Makerspace annual passes (MVP) and extends their existing web shop.
**Domain:** `my.experimenta.science`
**Status:** In Planning / Initial Setup
---
## 📋 Task Management & Progress Tracking
**WICHTIG:** Dieses Projekt nutzt ein strukturiertes Task-System im `tasks/` Ordner.
### Workflow für alle Implementierungen:
**1. Vor Start einer Arbeitssession:**
- Lies `tasks/00-PROGRESS.md` → Identifiziere aktuelle Phase
- Öffne die relevante Phase-Datei (z.B. `tasks/03-authentication.md`)
- Prüfe Dependencies der Phase (sind alle abhängigen Phasen abgeschlossen?)
**2. Während der Implementierung:**
- Arbeite Tasks sequenziell ab (von oben nach unten in der Phase-Datei)
- Markiere abgeschlossene Tasks: `- [ ]``- [x]`
- Aktualisiere Progress-Zeile in Phase-Datei: `Progress: X/Y tasks (Z%)`
- Dokumentiere wichtige Entscheidungen im Notes-Bereich
- Bei Blocker: Status auf 🚫 setzen, Grund dokumentieren
**3. Nach jedem abgeschlossenen Task:**
- Update Phase-Datei (Task checkbox + Progress-Zeile)
- Update `tasks/00-PROGRESS.md`:
- Aktualisiere Progress in "Quick Status" Tabelle
- Aktualisiere "Current Work" Section (welche Phase, welcher Task, nächste Schritte)
- Falls Phase abgeschlossen: Status auf ✅, Started/Completed Datum eintragen
**4. Phase-Abschluss:**
- Prüfe: Alle Acceptance Criteria erfüllt?
- Setze Status in Phase-Datei auf ✅ Done
- Setze Status in `00-PROGRESS.md` auf ✅ Done
- Trage "Completed" Datum ein
- **⚠️ Frage den Benutzer explizit, ob mit der nächsten Phase fortgefahren werden soll**
- **Erst nach Bestätigung:** Identifiziere nächste Phase und starte mit Schritt 1
### Niemals vergessen:
- ⚠️ **Immer** `00-PROGRESS.md` aktualisieren nach jedem abgeschlossenen Task
- ⚠️ **Immer** Phase-Datei und PROGRESS.md synchron halten
- ⚠️ **Immer** bei Unterbrechung "Current Work" in PROGRESS.md dokumentieren
- ⚠️ **Niemals** mehrere Phasen gleichzeitig bearbeiten (sequenziell arbeiten)
**Siehe:** [`tasks/README.md`](./tasks/README.md) für vollständige Dokumentation des Task-Systems.
---
## Code Style Requirements
- **All code comments MUST be written in English**
- User-facing content (UI text, emails) should be in German
- Documentation should be in German (PRD, README)
## Tech Stack
- **Framework:** Nuxt 4 (Vue 3 Composition API + TypeScript)
- **UI Library:** shadcn-nuxt (<https://nuxt.com/modules/shadcn>)
- **Styling:** Tailwind CSS v4
- **Database:** PostgreSQL 16+
- **ORM:** Drizzle ORM (TypeScript-first, performant)
- **Queue System:** BullMQ (MIT License) - Async job processing
- **In-Memory Store:** Redis 7 - Queue storage, sessions, caching
- **Auth:** Cidaas (OIDC/OAuth2) - external platform
- **Payment:** PayPal (MVP), more providers later
- **i18n:** @nuxtjs/i18n - German (default) + English
- **Validation:** Zod + VeeValidate
- **State Management:** Pinia (minimal use, prefer composables)
- **Testing:** Vitest (unit/integration), Playwright (E2E)
- **Deployment:** Docker on Hetzner Proxmox
- **CI/CD:** GitLab
## Architecture Principles
### Authentication & User Management
**Critical:** Cidaas is **only** for authentication (login/registration). User profiles and roles are stored in the local PostgreSQL database.
- Cidaas provides: OAuth2 authentication, user identity
- Local DB stores: User profile, roles, preferences, purchase history
- Custom UI for login/registration (not Cidaas hosted pages)
- Link users via `experimenta_id` field
### Data Flow
1. **Product Sync:** NAV ERP <20> Push to `/api/erp/products` <20> PostgreSQL
2. **Order Flow:** User checkout <20> PayPal payment <20> Order in DB <20> **X-API** <20> NAV ERP
3. **Auth Flow:** User login <20> Cidaas OAuth <20> Create/update local user profile <20> Session
### Key Integrations
- **NAV ERP:** Microsoft Dynamics NAV - Push products/stock to our API
- **X-API:** Order submission to NAV ERP via `/shopware/order` endpoint
- Environments: x-api-{dev|stage|live}.experimenta.science
- **Authentication:** HTTP Basic Auth (username + password)
- Credentials stored in environment variables (never hardcoded)
- Converts our JSON orders to SOAP for NAV ERP
- Critical: Prices must be in cents (Integer), dates in ISO 8601 UTC
- **Cidaas:** Auth platform by Widas (company requirement)
## MVP Scope (Phase 1)
**In Scope:**
- User registration and login (email-based via Cidaas)
- Display Makerspace annual passes ("Makerspace-Jahreskarten")
- Shopping cart functionality
- Checkout process
- PayPal payment integration
- NAV ERP product sync (receive products via push)
**Out of Scope (Post-MVP):**
- User roles (Educator, Company) - MVP only supports "Privatperson" implicitly
- Educator annual passes ("Pädagogische Jahreskarten")
- Approval workflows
- Experimenta tickets + Science Dome seat reservations
- Lab courses for schools
- Multi-payment providers
## Project Structure (Once Initialized)
```
/
├── app/
│ ├── components/ # Vue components (Nuxt 4 structure)
│ │ └── ui/ # shadcn-nuxt components
│ ├── layouts/ # Nuxt layouts
│ ├── lib/ # Client-side utilities
│ │ └── utils.ts # Shared utilities
│ └── pages/ # File-based routing (Nuxt 4)
├── server/
│ ├── api/ # API routes (Nitro)
│ │ ├── auth/ # Cidaas OAuth handlers
│ │ ├── products/ # Product endpoints
│ │ ├── cart/ # Shopping cart
│ │ ├── orders/ # Order management
│ │ ├── payment/ # PayPal integration
│ │ └── erp/ # NAV ERP endpoints (API key protected)
│ ├── database/
│ │ ├── schema.ts # Drizzle schema definitions
│ │ └── migrations/ # DB migrations
│ └── utils/ # Server-side utilities
├── components/ # Legacy components (to be migrated to app/)
├── composables/ # Vue composables
├── pages/ # Legacy pages (to be migrated to app/)
├── docs/ # Documentation
│ ├── PRD.md # Product Requirements Document
│ ├── TECH_STACK.md # Tech decisions & rationale
│ └── ARCHITECTURE.md # System architecture
└── docker-compose.yml # Docker setup
```
## Database Schema
See `docs/ARCHITECTURE.md` for full schema. Key tables:
- `users` - User profiles (linked to Cidaas via `experimenta_id`)
- **Includes billing address fields:** `salutation`, `date_of_birth`, `street`, `post_code`, `city`, `country_code`
- Address fields are optional, filled during checkout or profile edit
- Pre-fills checkout form for returning customers
- `products` - Products synced from NAV ERP
- `carts` / `cart_items` - Shopping cart
- `orders` / `order_items` - Orders and line items
Use Drizzle ORM for all database operations. All tables use UUID primary keys.
## Development Commands (Once Set Up)
```bash
# Install dependencies
pnpm install
# Development server
pnpm dev
# Build for production
pnpm build
# Run production build locally
pnpm preview
# Database migrations
pnpm db:generate # Generate migrations from schema changes
pnpm db:migrate # Apply migrations to database
pnpm db:studio # Open Drizzle Studio (DB GUI)
# Testing
pnpm test # Run unit tests (Vitest)
pnpm test:e2e # Run E2E tests (Playwright)
pnpm test:coverage # Generate coverage report
# Code quality
pnpm lint # ESLint
pnpm format # Prettier
pnpm typecheck # TypeScript type checking
```
## Development Tools & Testing
### Playwright MCP Server
Ein Playwright MCP Server ist installiert und verfügbar für:
- **UI/UX Überprüfung:** Visuelle Validierung von Komponenten und Layouts
- **Funktionale Tests:** Interaktive Tests während der Entwicklung
- **Responsive Design Checks:** Überprüfung der Mobile-First Implementierung
- **User Flow Testing:** Validierung kompletter Benutzer-Workflows (z.B. Checkout-Prozess)
**Wann nutzen:**
- Nach Implementierung neuer UI-Komponenten
- Bei Layout-Anpassungen (besonders mobile vs. desktop)
- Zum Testen von Formularen und Interaktionen
- Zur Validierung des gesamten Checkout-Flows
- Bei Integration von shadcn-nuxt Komponenten
**Workflow:**
1. Starte den Dev-Server (`pnpm dev`)
2. Nutze Playwright MCP Tools um die Anwendung zu navigieren und zu testen
3. Validiere visuelle Darstellung und Funktionalität
4. Dokumentiere gefundene Issues oder bestätige erfolgreiche Implementierung
## Important Constraints
1. **Cidaas Custom UI:** We must implement custom login/registration forms in our app (not hosted by Cidaas). Use OIDC/OAuth2 flow with Cidaas as identity provider.
2. **Mobile-First:** All UI must be optimized for mobile devices first, then scale up to desktop.
3. **Corporate Design:** Use experimenta style guide for colors and fonts (configured in Tailwind).
4. **NAV ERP Integration:** Products are pushed to us (we don't pull). Implement robust endpoint with API key authentication and validation.
5. **No Roles in MVP:** Don't implement role selection or role-based features yet. All users are implicitly "Privatperson" (private individual).
6. **TypeScript Strict Mode:** Use strict TypeScript everywhere. All schemas defined with Zod.
7. **Bilingual Support:** App must support German (default) and English. Use @nuxtjs/i18n for all user-facing text. Routes: `/produkte` (de), `/en/products` (en).
## Security Considerations
- Never commit secrets (`.env` is gitignored)
- All API endpoints must validate input (use Zod schemas)
- ERP endpoints must be protected with API key authentication
- Use session-based auth (HTTP-only cookies)
- Validate JWT tokens from Cidaas using `jose` library
- Implement rate limiting on public endpoints
## Common Patterns
### API Route Example
```typescript
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
// Validate path params with Zod
const { id } = await getValidatedRouterParams(
event,
z.object({
id: z.string().uuid(),
}).parse
)
// Query with Drizzle
const product = await db.query.products.findFirst({
where: eq(products.id, id),
})
if (!product) {
throw createError({
statusCode: 404,
message: 'Product not found',
})
}
return product
})
```
### Protected Route Example
```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
const { user } = await useAuth()
if (!user.value) {
return navigateTo('/login')
}
})
```
### Checkout with Saved Address Pattern
```typescript
// pages/checkout.vue
<script setup lang="ts">
const { user } = await useAuth()
const { saveAddress } = useUserProfile()
// Pre-fill form if user has saved address
const form = reactive({
salutation: user.value?.salutation || '',
dateOfBirth: user.value?.dateOfBirth || '',
street: user.value?.street || '',
postCode: user.value?.postCode || '',
city: user.value?.city || '',
countryCode: user.value?.countryCode || 'DE',
// Pre-checked if user doesn't have address yet
saveForFuture: !user.value?.street
})
async function handleCheckout() {
// Validate form
const validated = await checkoutSchema.parseAsync(form)
// Save address to profile if checkbox is checked
if (form.saveForFuture) {
await saveAddress({
salutation: validated.salutation,
dateOfBirth: validated.dateOfBirth,
street: validated.street,
postCode: validated.postCode,
city: validated.city,
countryCode: validated.countryCode,
})
}
// Continue with payment
await processPayment(validated)
}
</script>
<template>
<form @submit.prevent="handleCheckout">
<h2>{{ $t('checkout.billingAddress') }}</h2>
<!-- Address form fields -->
<Input v-model="form.street" :label="$t('checkout.street')" required />
<Input v-model="form.postCode" :label="$t('checkout.postCode')" required />
<Input v-model="form.city" :label="$t('checkout.city')" required />
<!-- Save address checkbox -->
<Checkbox
v-model="form.saveForFuture"
:label="$t('checkout.saveAddress')"
/>
<Button type="submit">{{ $t('checkout.continue') }}</Button>
</form>
</template>
```
### X-API Order Transformation Pattern
```typescript
// server/utils/xapi.ts
// Transform order from DB to X-API format
export function transformOrderToXAPI(order: Order, user: User) {
return {
shopPOSOrder: {
documentType: 'Order',
externalDocumentNo: order.orderNumber,
salesChannel: 'Shop',
shoppingCartCompletion: order.createdAt.toISOString(),
visitType: 'Private',
amountIncludingVAT: Math.round(order.totalAmount * 100), // EUR -> Cents!
language: 'DEU',
salesLine: order.items.map((item, index) => ({
type: 'Item',
lineNo: String((index + 1) * 10000), // 10000, 20000, 30000...
no: item.product.navProductId,
quantity: item.quantity,
unitPrice: Math.round(item.priceSnapshot * 100), // EUR -> Cents!
vatPct: 7,
visitorCategory: '120', // Annual pass holders
ticketCode: generateUniqueTicketCode(),
// Required for annual passes!
annualPass: {
salutationCode: mapSalutation(user.salutation), // 'HERR', 'FRAU', 'K_ANGABE'
firstName: user.firstName,
lastName: user.lastName,
streetAndHouseNumber: user.address.street,
postCode: user.address.postCode,
city: user.address.city,
countryCode: user.address.countryCode || 'DE',
dateOfBirth: user.dateOfBirth,
eMail: user.email,
validFrom: new Date().toISOString().split('T')[0], // YYYY-MM-DD
},
})),
payment: [
{
paymentEntryNo: 10000,
amount: Math.round(order.totalAmount * 100), // EUR -> Cents!
paymentType: 'Shop PayPal',
createdOn: order.paymentCompletedAt.toISOString(),
reference: order.paymentId,
paymentId: order.paymentId,
},
],
personContact: {
experimentaAccountID: user.experimentaId, // Our experimenta_id!
eMail: user.email,
salutationCode: mapSalutation(user.salutation),
firstName: user.firstName,
lastName: user.lastName,
streetAndHouseNumber: order.billingAddress.street,
postCode: order.billingAddress.postCode,
city: order.billingAddress.city,
countryCode: order.billingAddress.countryCode || 'DE',
phoneNumber: user.phone || '',
guest: false,
},
},
}
}
// Submit to X-API with retry logic and Basic Auth
export async function submitOrderToXAPI(payload: XAPIOrderPayload) {
const config = useRuntimeConfig()
const maxRetries = 3
const retryDelays = [1000, 3000, 9000] // Exponential backoff
// Prepare Basic Auth header
const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(`${config.xApiBaseUrl}/shopware/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${authString}`,
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`X-API error ${response.status}: ${errorText}`)
}
return await response.json()
} catch (error) {
if (attempt === maxRetries - 1) throw error
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]))
}
}
}
```
**Critical transformations:**
- Prices: EUR (Decimal) → Cents (Integer) using `Math.round(price * 100)`
- Dates: JavaScript Date → ISO 8601 UTC using `.toISOString()`
- Line numbers: Sequential multiples of 10000
- Salutation: 'male' → 'HERR', 'female' → 'FRAU', other → 'K_ANGABE'
## Authentication Patterns
See [`docs/CIDAAS_INTEGRATION.md`](./docs/CIDAAS_INTEGRATION.md) for complete Cidaas OAuth2 implementation guide.
### OAuth2 Login Flow Pattern
```typescript
// composables/useAuth.ts - Client-side auth composable
export function useAuth() {
const { loggedIn, user, clear, fetch } = useUserSession()
async function login(email: string) {
// Initiate OAuth2 Authorization Code Flow with PKCE
const { redirectUrl } = await $fetch('/api/auth/login', {
method: 'POST',
body: { email },
})
// Redirect to Cidaas login page
navigateTo(redirectUrl, { external: true })
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
await clear()
navigateTo('/')
}
return { user, loggedIn, login, logout }
}
```
**Server-side:**
```typescript
// server/api/auth/login.post.ts - Initiate OAuth2 flow
export default defineEventHandler(async (event) => {
const { email } = await readBody(event)
// 1. Generate PKCE challenge
const { verifier, challenge } = await generatePKCE()
// 2. Store PKCE verifier in cookie (5min TTL)
setCookie(event, 'pkce_verifier', verifier, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 300,
})
// 3. Generate state (CSRF protection)
const state = generateState(32)
setCookie(event, 'oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 300,
})
// 4. Build Cidaas authorization URL
const config = useRuntimeConfig()
const authUrl = new URL(config.cidaas.authorizeUrl)
authUrl.searchParams.set('client_id', config.cidaas.clientId)
authUrl.searchParams.set('redirect_uri', config.cidaas.redirectUri)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid profile email')
authUrl.searchParams.set('state', state)
authUrl.searchParams.set('code_challenge', challenge)
authUrl.searchParams.set('code_challenge_method', 'S256')
authUrl.searchParams.set('login_hint', email)
return { redirectUrl: authUrl.toString() }
})
```
### OAuth2 Callback Pattern
```typescript
// server/api/auth/callback.get.ts - Handle OAuth2 callback
export default defineEventHandler(async (event) => {
const { code, state } = getQuery(event)
// 1. Validate state (CSRF protection)
const storedState = getCookie(event, 'oauth_state')
if (!storedState || state !== storedState) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state parameter',
})
}
// 2. Retrieve PKCE verifier
const verifier = getCookie(event, 'pkce_verifier')
if (!verifier) {
throw createError({
statusCode: 400,
statusMessage: 'PKCE verifier not found',
})
}
try {
// 3. Exchange code for tokens
const tokens = await exchangeCodeForToken(code as string, verifier)
// 4. Validate ID token
const idTokenPayload = await verifyIdToken(tokens.id_token)
// 5. Fetch user info from Cidaas
const cidaasUser = await fetchUserInfo(tokens.access_token)
// 6. Create/update user in local DB
const db = useDatabase()
let user = await db.query.users.findFirst({
where: eq(users.experimentaId, cidaasUser.sub),
})
if (!user) {
// First time login - create user
const [newUser] = await db
.insert(users)
.values({
experimentaId: cidaasUser.sub,
email: cidaasUser.email,
firstName: cidaasUser.given_name,
lastName: cidaasUser.family_name,
})
.returning()
user = newUser
} else {
// Update last login
await db.update(users).set({ updatedAt: new Date() }).where(eq(users.id, user.id))
}
// 7. Create encrypted session
await setUserSession(event, {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
},
})
// 8. Clean up temporary cookies
deleteCookie(event, 'oauth_state')
deleteCookie(event, 'pkce_verifier')
// 9. Redirect to homepage
return sendRedirect(event, '/')
} catch (error) {
console.error('OAuth callback error:', error)
deleteCookie(event, 'oauth_state')
deleteCookie(event, 'pkce_verifier')
return sendRedirect(event, '/auth?error=login_failed')
}
})
```
### Protected Route Middleware Pattern
```typescript
// middleware/auth.ts - Require authentication
export default defineNuxtRouteMiddleware(async (to, from) => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
// Store intended destination for post-login redirect
useCookie('redirect_after_login', {
maxAge: 600, // 10 minutes
path: '/',
}).value = to.fullPath
return navigateTo('/auth')
}
})
```
**Usage in pages:**
```vue
<!-- pages/profile.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth', // Require authentication
})
const { user } = useAuth()
</script>
<template>
<div>
<h1>Welcome, {{ user.firstName }}!</h1>
</div>
</template>
```
### Protected API Endpoint Pattern
```typescript
// server/api/orders/index.get.ts - Protected API route
export default defineEventHandler(async (event) => {
// Require authentication (throws 401 if not logged in)
const { user } = await requireUserSession(event)
// Fetch user's orders
const db = useDatabase()
const orders = await db.query.orders.findMany({
where: eq(orders.userId, user.id),
orderBy: desc(orders.createdAt),
})
return orders
})
```
### User Registration Pattern
```typescript
// server/api/auth/register.post.ts - Register via Cidaas
import { z } from 'zod'
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[a-z]/).regex(/[0-9]/),
firstName: z.string().min(2),
lastName: z.string().min(2),
})
export default defineEventHandler(async (event) => {
// Validate input
const body = await readBody(event)
const validated = registerSchema.parse(body)
// Register user via Cidaas API
const result = await registerUser({
email: validated.email,
password: validated.password,
given_name: validated.firstName,
family_name: validated.lastName,
locale: 'de',
})
return {
success: true,
message: 'Registration successful. Please verify your email.',
}
})
```
### Session Management Pattern
```typescript
// Server-side: Create session
await setUserSession(event, {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
},
loggedInAt: new Date().toISOString(),
})
// Server-side: Check session
const { user } = await getUserSession(event)
if (!user) {
throw createError({ statusCode: 401, message: 'Not authenticated' })
}
// Server-side: Require session
const { user } = await requireUserSession(event) // Throws 401 if not logged in
// Server-side: Clear session
await clearUserSession(event)
```
```vue
<!-- Client-side: Use session in components -->
<script setup lang="ts">
const { user, loggedIn } = useUserSession()
// user.value: { id, email, firstName, ... } or null
// loggedIn.value: boolean
</script>
<template>
<div v-if="loggedIn">
<p>Welcome {{ user.firstName }}!</p>
</div>
<div v-else>
<NuxtLink to="/auth">Login</NuxtLink>
</div>
</template>
```
### JWT Validation Pattern
```typescript
// server/utils/jwt.ts - Validate Cidaas ID tokens
import { jwtVerify, createRemoteJWKSet } from 'jose'
const config = useRuntimeConfig()
const JWKS = createRemoteJWKSet(new URL(config.cidaas.jwksUrl))
export async function verifyIdToken(idToken: string) {
try {
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: config.cidaas.issuer,
audience: config.cidaas.clientId,
})
return payload
} catch (error) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid ID token',
})
}
}
```
### Rate Limiting Pattern
```typescript
// server/middleware/rate-limit.ts - Rate limit auth endpoints
interface RateLimitEntry {
count: number
resetAt: number
}
const rateLimitStore = new Map<string, RateLimitEntry>()
export default defineEventHandler((event) => {
const path = event.path
if (!path.startsWith('/api/auth/')) return
const ip = getRequestIP(event) || 'unknown'
const limits = {
'/api/auth/login': { maxAttempts: 5, windowMs: 15 * 60 * 1000 },
'/api/auth/register': { maxAttempts: 3, windowMs: 60 * 60 * 1000 },
}
const limit = limits[path]
if (!limit) return
const key = `${ip}:${path}`
const now = Date.now()
const entry = rateLimitStore.get(key)
if (!entry || entry.resetAt < now) {
rateLimitStore.set(key, { count: 1, resetAt: now + limit.windowMs })
return
}
entry.count++
if (entry.count > limit.maxAttempts) {
throw createError({
statusCode: 429,
statusMessage: 'Too many requests',
})
}
})
```
### Key Security Practices
1. **PKCE for OAuth2:**
- Always use PKCE (Proof Key for Code Exchange)
- Prevents authorization code interception attacks
- Generate random verifier, compute SHA-256 challenge
- Store verifier in temporary cookie (5min TTL)
2. **State Parameter:**
- Generate random state for CSRF protection
- Validate state on callback
- Store in temporary cookie (5min TTL)
3. **Encrypted Sessions:**
- Use `nuxt-auth-utils` for session management
- Sessions encrypted with AES-256-GCM
- HTTP-only, Secure, SameSite=Lax cookies
- 30-day expiration (configurable)
4. **Rate Limiting:**
- Login: 5 attempts / 15min per IP
- Register: 3 attempts / hour per IP
- Implement in middleware, not individual endpoints
5. **Input Validation:**
- Always validate with Zod schemas
- Validate on both client and server
- Never trust client-side validation alone
**Implementation Details:**
For complete implementation with all utilities (PKCE generator, Cidaas API client, full endpoint code), see:
- [`docs/CIDAAS_INTEGRATION.md`](./docs/CIDAAS_INTEGRATION.md) - Complete implementation guide
- [`docs/ARCHITECTURE.md#3.6`](./docs/ARCHITECTURE.md#36-authentication--authorization-cidaas-oauth2oidc) - Architecture diagrams and flows
## Documentation
- **PRD:** See `docs/PRD.md` for complete requirements
- **Tech Stack:** See `docs/TECH_STACK.md` for technology decisions
- **Architecture:** See `docs/ARCHITECTURE.md` for system design and data flows
## Deployment
- **Platform:** Docker containers on Hetzner Proxmox
- **CI/CD:** GitLab pipelines (build <20> test <20> deploy)
- **Environments:** Staging (auto-deploy) + Production (manual approval)
- **Database Backups:** Daily automated backups with 7-day retention
## Future Phases
After MVP, we'll add:
- **Phase 2:** Educator roles + approval workflow + educator annual passes
- **Phase 3:** Experimenta tickets + Science Dome seat reservations
- **Phase 4:** Lab courses for schools
Keep the codebase modular to accommodate these features without major refactoring.
- Always use context7 when I need code generation, setup or configuration steps, or
library/API documentation. This means you should automatically use the Context7 MCP
tools to resolve library id and get library docs without me having to explicitly ask.