foamking/docs/plans/2026-02-22-delivery-cleanup.md
mikl0s 7eae4fde33 Add delivery cleanup implementation plan
12-task plan covering route restructure, auth simplification,
DB cleanup, security hardening, debug stripping, setup docs,
and build script for zip delivery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:35:06 +00:00

1031 lines
28 KiB
Markdown

# FoamKing Delivery Cleanup Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Prepare the FoamKing calculator for zip delivery — restructure routes under `/intern/*`, replace DB-based auth with env-based single admin user, clean up the database layer, strip debug code, harden security, write setup docs, and create a build script.
**Architecture:** Next.js app with SQLite (better-sqlite3). Auth simplified from DB-backed bcrypt sessions to env-based credentials with in-memory session Map. All protected routes consolidated under `/intern/*`. Build script produces a clean zip with no git history, no docs, no secrets.
**Tech Stack:** Next.js 16, TypeScript, SQLite (better-sqlite3), Tailwind CSS, shadcn/ui, nodemailer
**Design doc:** `docs/plans/2026-02-22-delivery-cleanup-design.md`
---
### Task 1: Rewrite lib/auth.ts — env-based auth with swappable interface
**Files:**
- Rewrite: `lib/auth.ts`
**Step 1: Rewrite lib/auth.ts**
Replace the entire file with:
```typescript
import { cookies } from "next/headers"
const SESSION_COOKIE_NAME = "session"
const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
// In-memory session store. Replace this with your own session/token system.
const sessions = new Map<string, { email: string; expiresAt: Date }>()
function generateSessionId(): string {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}
// ─── AUTH INTERFACE ────────────────────────────────────────────────
// To integrate your own auth (JWT, OAuth, etc.), replace these functions.
// The rest of the application only calls checkAuth(), login(), and logout().
// ───────────────────────────────────────────────────────────────────
/**
* Check if the current request is authenticated.
* Replace this function to integrate JWT, OAuth, or any other auth system.
*/
export async function checkAuth(): Promise<{ authenticated: boolean; email?: string }> {
const cookieStore = await cookies()
const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value
if (!sessionId) return { authenticated: false }
const session = sessions.get(sessionId)
if (!session) return { authenticated: false }
if (session.expiresAt < new Date()) {
sessions.delete(sessionId)
return { authenticated: false }
}
return { authenticated: true, email: session.email }
}
/**
* Authenticate with email and password.
* Default: compares against ADMIN_EMAIL/ADMIN_PASSWORD env vars.
* Replace this function to integrate your own auth system.
*/
export async function login(
email: string,
password: string
): Promise<{ success: true } | { success: false; error: string }> {
const adminEmail = process.env.ADMIN_EMAIL
const adminPassword = process.env.ADMIN_PASSWORD
if (!adminEmail || !adminPassword) {
return { success: false, error: "Admin-konfiguration mangler (ADMIN_EMAIL/ADMIN_PASSWORD)" }
}
if (email !== adminEmail || password !== adminPassword) {
return { success: false, error: "Forkert email eller adgangskode" }
}
const sessionId = generateSessionId()
const expiresAt = new Date(Date.now() + SESSION_DURATION_MS)
sessions.set(sessionId, { email, expiresAt })
const cookieStore = await cookies()
cookieStore.set(SESSION_COOKIE_NAME, sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
expires: expiresAt,
path: "/",
})
return { success: true }
}
/**
* Log out the current session.
*/
export async function logout(): Promise<void> {
const cookieStore = await cookies()
const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value
if (sessionId) {
sessions.delete(sessionId)
cookieStore.delete(SESSION_COOKIE_NAME)
}
}
```
**Step 2: Verify the app still compiles**
Run: `npm run build`
Expected: Build errors related to removed DB imports (this is expected — we fix them in later tasks)
**Step 3: Commit**
```bash
git add lib/auth.ts
git commit -m "refactor: replace DB-based auth with env-based single admin user"
```
---
### Task 2: Rewrite lib/db.ts — clean schema, remove users/sessions
**Files:**
- Rewrite: `lib/db.ts`
**Step 1: Rewrite lib/db.ts**
Replace the entire file. Remove users table, sessions table, all user/session functions, and migration hacks. Keep only quotes functionality:
```typescript
import Database from "better-sqlite3"
import path from "path"
import fs from "fs"
const DB_PATH = path.join(process.cwd(), "data", "quotes.db")
// Ensure data directory exists
const dataDir = path.dirname(DB_PATH)
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}
const db = new Database(DB_PATH)
// Enable WAL mode for better concurrent read performance
db.pragma("journal_mode = WAL")
// Create schema if not exists
db.exec(`
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postal_code TEXT NOT NULL,
address TEXT,
area REAL NOT NULL,
height REAL NOT NULL,
include_floor_heating INTEGER DEFAULT 1,
flooring_type TEXT DEFAULT 'STANDARD',
customer_name TEXT NOT NULL,
customer_email TEXT NOT NULL,
customer_phone TEXT NOT NULL,
remarks TEXT,
total_excl_vat REAL NOT NULL,
total_incl_vat REAL NOT NULL,
status TEXT DEFAULT 'new',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
email_opened_at TEXT
)
`)
// Ensure quote IDs start at 1000
const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number }
if (countResult.count === 0) {
try {
const seqExists = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'quotes'").get()
if (!seqExists) {
db.exec(
"INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)"
)
db.exec("DELETE FROM quotes WHERE id = 999")
}
} catch {
// sqlite_sequence may not exist yet on fresh DB — that's fine
}
}
// ─── Types ─────────────────────────────────────────────────────────
export interface QuoteInput {
postalCode: string
address?: string
area: number
height: number
includeFloorHeating: boolean
flooringType: string
customerName: string
customerEmail: string
customerPhone: string
remarks?: string
totalExclVat: number
totalInclVat: number
}
export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected"
export interface StoredQuote extends QuoteInput {
id: number
status: QuoteStatus
createdAt: string
emailOpenedAt: string | null
}
// ─── Queries ───────────────────────────────────────────────────────
export function saveQuote(quote: QuoteInput): { id: number; slug: string } {
const stmt = db.prepare(`
INSERT INTO quotes (
postal_code, address, area, height, include_floor_heating, flooring_type,
customer_name, customer_email, customer_phone, remarks, total_excl_vat, total_incl_vat
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
const result = stmt.run(
quote.postalCode,
quote.address || null,
quote.area,
quote.height,
quote.includeFloorHeating ? 1 : 0,
quote.flooringType,
quote.customerName,
quote.customerEmail,
quote.customerPhone,
quote.remarks || null,
quote.totalExclVat,
quote.totalInclVat
)
const id = result.lastInsertRowid as number
const slug = `${quote.postalCode}-${id}`
return { id, slug }
}
export function getQuoteBySlug(slug: string): StoredQuote | null {
const match = slug.match(/^(\d{4})-(\d+)$/)
if (!match) return null
const [, postalCode, idStr] = match
const id = parseInt(idStr, 10)
const row = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?").get(id, postalCode) as any
if (!row) return null
return rowToQuote(row)
}
export function getQuoteById(id: number): StoredQuote | null {
const row = db.prepare("SELECT * FROM quotes WHERE id = ?").get(id) as any
if (!row) return null
return rowToQuote(row)
}
export function getAllQuotes(): StoredQuote[] {
const rows = db.prepare("SELECT * FROM quotes ORDER BY id DESC").all() as any[]
return rows.map(rowToQuote)
}
export function updateQuoteStatus(id: number, status: QuoteStatus): boolean {
const result = db.prepare("UPDATE quotes SET status = ? WHERE id = ?").run(status, id)
return result.changes > 0
}
export function markEmailOpened(id: number): boolean {
const result = db.prepare(
"UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL"
).run(new Date().toISOString(), id)
return result.changes > 0
}
function rowToQuote(row: any): StoredQuote {
return {
id: row.id,
postalCode: row.postal_code,
address: row.address,
area: row.area,
height: row.height,
includeFloorHeating: row.include_floor_heating === 1,
flooringType: row.flooring_type,
customerName: row.customer_name,
customerEmail: row.customer_email,
customerPhone: row.customer_phone,
remarks: row.remarks,
totalExclVat: row.total_excl_vat,
totalInclVat: row.total_incl_vat,
status: row.status || "new",
createdAt: row.created_at,
emailOpenedAt: row.email_opened_at || null,
}
}
```
**Step 2: Commit**
```bash
git add lib/db.ts
git commit -m "refactor: clean DB schema, remove users/sessions tables"
```
---
### Task 3: Delete /api/auth/setup, update login and logout routes
**Files:**
- Delete: `app/api/auth/setup/route.ts`
- Modify: `app/api/auth/login/route.ts`
- Modify: `app/api/auth/logout/route.ts`
**Step 1: Delete the setup endpoint**
```bash
rm app/api/auth/setup/route.ts
```
**Step 2: Rewrite login route**
Replace `app/api/auth/login/route.ts` with:
```typescript
import { NextRequest, NextResponse } from "next/server"
import { login } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json()
if (!email || !password) {
return NextResponse.json({ error: "Email og adgangskode er påkrævet" }, { status: 400 })
}
const result = await login(email, password)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 401 })
}
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Der opstod en fejl" }, { status: 500 })
}
}
```
**Step 3: Rewrite logout route**
Replace `app/api/auth/logout/route.ts` with:
```typescript
import { NextResponse } from "next/server"
import { logout } from "@/lib/auth"
export async function POST() {
try {
await logout()
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Der opstod en fejl" }, { status: 500 })
}
}
```
**Step 4: Commit**
```bash
git add -A app/api/auth/
git commit -m "refactor: simplify auth routes, remove setup endpoint"
```
---
### Task 4: Move routes — /admin → /intern/beregner, /dashboard → /intern, /historik → /intern/historik, /login → /intern/login
**Files:**
- Move: `app/admin/page.tsx``app/intern/beregner/page.tsx`
- Move: `app/dashboard/page.tsx``app/intern/page.tsx`
- Move: `app/historik/page.tsx``app/intern/historik/page.tsx`
- Move: `app/login/page.tsx``app/intern/login/page.tsx`
**Step 1: Create new directories and move files**
```bash
mkdir -p app/intern/beregner app/intern/historik app/intern/login
mv app/admin/page.tsx app/intern/beregner/page.tsx
mv app/dashboard/page.tsx app/intern/page.tsx
mv app/historik/page.tsx app/intern/historik/page.tsx
mv app/login/page.tsx app/intern/login/page.tsx
rmdir app/admin app/dashboard app/historik app/login
```
**Step 2: Update all internal links in moved files**
In `app/intern/page.tsx` (was dashboard):
- Change `router.push("/login")``router.push("/intern/login")`
- Change `<Link href="/historik">``<Link href="/intern/historik">`
In `app/intern/historik/page.tsx` (was historik):
- Change `router.push("/login")``router.push("/intern/login")`
- Change `<Link href="/dashboard">``<Link href="/intern">`
In `app/intern/login/page.tsx` (was login):
- Change `fetch("/api/auth/login"` — keep as-is (API routes don't move)
- Change `router.push("/dashboard")``router.push("/intern")`
In `app/intern/beregner/page.tsx` (was admin):
- No route references to update
**Step 3: Update middleware.ts**
Replace entire file:
```typescript
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Don't protect the login page
if (pathname === "/intern/login") {
return NextResponse.next()
}
// Check for session cookie on all /intern/* routes
const sessionCookie = request.cookies.get("session")
if (!sessionCookie?.value) {
const loginUrl = new URL("/intern/login", request.url)
loginUrl.searchParams.set("redirect", pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ["/intern/:path*"],
}
```
**Step 4: Commit**
```bash
git add -A
git commit -m "refactor: consolidate protected routes under /intern/*"
```
---
### Task 5: Strip debug code — console.logs, dev toggles, TODO markers
**Files:**
- Modify: `app/intern/beregner/page.tsx` — remove console.log and "Admin Mode" toggle reference if not needed publicly
- Modify: `app/intern/page.tsx` — remove console.error
- Modify: `app/intern/historik/page.tsx` — remove console.error
- Modify: `app/api/quotes/route.ts` — remove console.error
- Modify: `app/api/quote-request/route.ts` — remove console.error
**Step 1: Remove all console.log/console.error statements**
In each file listed above, find and remove all `console.log(...)` and `console.error(...)` lines. In catch blocks, keep the error handling logic but remove the console output.
Known locations:
- `app/intern/beregner/page.tsx:39``console.log("Quote request (test mode):", {...})`
- `app/intern/page.tsx:49``console.error("Failed to fetch quotes:", error)`
- `app/intern/page.tsx:90``console.error("Logout failed:", error)`
- `app/intern/historik/page.tsx:51``console.error("Failed to fetch quotes:", error)`
- `app/intern/historik/page.tsx:63``console.error("Logout failed:", error)`
- `app/api/quote-request/route.ts:300``console.error("Quote request error:", error)`
- `app/api/quotes/route.ts:15``console.error("Get quotes error:", error)`
- `app/api/quotes/route.ts:45``console.error("Update quote error:", error)`
**Step 2: Commit**
```bash
git add -A
git commit -m "chore: strip debug console.log/error statements"
```
---
### Task 6: Add rate limiting to API routes
**Files:**
- Create: `lib/rate-limit.ts`
- Modify: `app/api/auth/login/route.ts`
- Modify: `app/api/quote-request/route.ts`
**Step 1: Create lib/rate-limit.ts**
```typescript
// Simple in-memory rate limiter. No external dependencies.
// Tracks requests per IP with a sliding window.
interface RateLimitEntry {
count: number
resetAt: number
}
const store = new Map<string, RateLimitEntry>()
// Clean up expired entries periodically
setInterval(() => {
const now = Date.now()
for (const [key, entry] of store) {
if (entry.resetAt < now) store.delete(key)
}
}, 60_000)
/**
* Check if a request should be rate-limited.
* @param key - Unique identifier (e.g. IP address)
* @param limit - Max requests per window
* @param windowMs - Time window in milliseconds
* @returns true if the request is allowed, false if rate-limited
*/
export function rateLimit(key: string, limit: number, windowMs: number): boolean {
const now = Date.now()
const entry = store.get(key)
if (!entry || entry.resetAt < now) {
store.set(key, { count: 1, resetAt: now + windowMs })
return true
}
if (entry.count >= limit) {
return false
}
entry.count++
return true
}
```
**Step 2: Add rate limiting to login route**
In `app/api/auth/login/route.ts`, add at the top of the POST handler:
```typescript
import { rateLimit } from "@/lib/rate-limit"
// Inside POST handler, before processing:
const ip = request.headers.get("x-forwarded-for") || "unknown"
if (!rateLimit(ip, 5, 60_000)) {
return NextResponse.json({ error: "For mange forsøg. Prøv igen om lidt." }, { status: 429 })
}
```
**Step 3: Add rate limiting to quote-request route**
In `app/api/quote-request/route.ts`, add at the top of the POST handler:
```typescript
import { rateLimit } from "@/lib/rate-limit"
// Inside POST handler, before processing:
const ip = request.headers.get("x-forwarded-for") || "unknown"
if (!rateLimit(ip, 10, 60_000)) {
return NextResponse.json({ error: "For mange anmodninger. Prøv igen om lidt." }, { status: 429 })
}
```
**Step 4: Commit**
```bash
git add lib/rate-limit.ts app/api/auth/login/route.ts app/api/quote-request/route.ts
git commit -m "feat: add rate limiting to login and quote-request endpoints"
```
---
### Task 7: Create npm run setup seed script
**Files:**
- Create: `scripts/setup.js`
- Modify: `package.json` — add `"setup"` script
**Step 1: Create scripts/setup.js**
```javascript
#!/usr/bin/env node
const Database = require("better-sqlite3")
const path = require("path")
const fs = require("fs")
const DB_PATH = path.join(process.cwd(), "data", "quotes.db")
const dataDir = path.dirname(DB_PATH)
console.log("FoamKing Prisberegner - Database Setup")
console.log("======================================\n")
// Create data directory
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
console.log("Created data/ directory")
}
// Check if DB already exists
const dbExists = fs.existsSync(DB_PATH)
if (dbExists) {
console.log("Database already exists at", DB_PATH)
console.log("Delete data/quotes.db to start fresh.\n")
process.exit(0)
}
const db = new Database(DB_PATH)
db.pragma("journal_mode = WAL")
db.exec(`
CREATE TABLE quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postal_code TEXT NOT NULL,
address TEXT,
area REAL NOT NULL,
height REAL NOT NULL,
include_floor_heating INTEGER DEFAULT 1,
flooring_type TEXT DEFAULT 'STANDARD',
customer_name TEXT NOT NULL,
customer_email TEXT NOT NULL,
customer_phone TEXT NOT NULL,
remarks TEXT,
total_excl_vat REAL NOT NULL,
total_incl_vat REAL NOT NULL,
status TEXT DEFAULT 'new',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
email_opened_at TEXT
)
`)
// Set auto-increment to start at 1000
db.exec(
"INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)"
)
db.exec("DELETE FROM quotes WHERE id = 999")
db.close()
console.log("Database created at", DB_PATH)
console.log("Quote IDs will start at 1000.\n")
console.log("Next steps:")
console.log(" 1. Configure .env.local (see SETUP.md / OPSÆTNING.md)")
console.log(" 2. npm run build")
console.log(" 3. npm start")
```
**Step 2: Add setup script to package.json**
Add to `"scripts"`:
```json
"setup": "node scripts/setup.js"
```
**Step 3: Commit**
```bash
git add scripts/setup.js package.json
git commit -m "feat: add npm run setup seed script for database initialization"
```
---
### Task 8: Update .env.example
**Files:**
- Rewrite: `.env.example`
**Step 1: Replace .env.example**
```bash
# ─── Admin Login ────────────────────────────────────────────────────
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=changeme
# ─── Email (SMTP) ──────────────────────────────────────────────────
# Office 365: smtp.office365.com, port 587
# Requires SMTP AUTH enabled for the sending account in Exchange Admin Center.
# See SETUP.md / OPSÆTNING.md for instructions.
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USER=tilbud@foamking.dk
SMTP_PASS=your_password_here
EMAIL_FROM_NAME=Foam King Prisberegner
EMAIL_TO=info@foamking.dk
# ─── Base URL ───────────────────────────────────────────────────────
NEXT_PUBLIC_BASE_URL=https://beregner.foamking.dk
# ─── Distance Calculation (Optional) ───────────────────────────────
# Without this key, distances are calculated from a built-in postal code table.
# Get a free key at https://openrouteservice.org/dev/#/signup (2,000 requests/day)
# OPENROUTE_API_KEY=your_api_key_here
```
**Step 2: Commit**
```bash
git add .env.example
git commit -m "docs: update .env.example with Office 365 SMTP and admin config"
```
---
### Task 9: Remove bcrypt dependency from package.json
**Files:**
- Modify: `package.json`
**Step 1: Remove bcrypt**
```bash
npm uninstall bcrypt @types/bcrypt
```
**Step 2: Verify build**
```bash
npm run build
```
Expected: Should compile successfully if all auth references are updated.
**Step 3: Commit**
```bash
git add package.json package-lock.json
git commit -m "chore: remove bcrypt dependency (auth is now env-based)"
```
---
### Task 10: Write SETUP.md and OPSÆTNING.md
**Files:**
- Create: `SETUP.md`
- Create: `OPSÆTNING.md`
**Step 1: Create SETUP.md**
```markdown
# FoamKing Prisberegner — Setup Guide
## Prerequisites
- Node.js 18+ (recommended: 20 LTS)
- npm
## Installation
1. **Install dependencies:**
```bash
npm install
```
2. **Configure environment:**
Copy the example config and edit it:
```bash
cp .env.example .env.local
```
Edit `.env.local` with your values. See the configuration section below.
3. **Initialize database:**
```bash
npm run setup
```
4. **Build and start:**
```bash
npm run build
npm start
```
The application runs on port 3001 by default.
## Configuration (.env.local)
### Admin Login (Required)
```
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your-secure-password
```
These credentials are used to log in at `/intern/login`.
### Email — Office 365 SMTP (Required)
```
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USER=tilbud@foamking.dk
SMTP_PASS=your-password
EMAIL_FROM_NAME=Foam King Prisberegner
EMAIL_TO=info@foamking.dk
```
**Important:** SMTP AUTH must be enabled for the sending account (`tilbud@foamking.dk`) in your Office 365 / Exchange Admin Center:
1. Go to [Exchange Admin Center](https://admin.exchange.microsoft.com)
2. Navigate to **Recipients** → **Mailboxes**
3. Select the `tilbud@foamking.dk` mailbox
4. Click **Manage email apps settings** (or **Mail flow** → **Email apps**)
5. Enable **Authenticated SMTP** (SMTP AUTH)
6. Save
If your organization uses Security Defaults or Conditional Access policies that block legacy auth, you may need to create an exclusion for this account, or use an App Password if MFA is enabled.
### Base URL (Required)
```
NEXT_PUBLIC_BASE_URL=https://beregner.foamking.dk
```
Used for generating links in emails (e.g., quote tracking links).
### Distance Calculation (Optional)
```
OPENROUTE_API_KEY=your_api_key_here
```
Without this key, transport distances are calculated from a built-in postal code table (sufficient for price estimates). With the key, actual driving distances from OpenRouteService are used.
Get a free key at [openrouteservice.org](https://openrouteservice.org/dev/#/signup) (2,000 requests/day).
## Routes
| Route | Description |
|-------|-------------|
| `/` | Public price calculator |
| `/tilbud/[id]` | Public quote view (linked from email) |
| `/intern` | Admin dashboard — quote management |
| `/intern/historik` | Admin — quote history |
| `/intern/beregner` | Admin — detailed calculator with price breakdown |
| `/intern/login` | Admin login page |
## Custom Authentication
The default auth uses `ADMIN_EMAIL`/`ADMIN_PASSWORD` environment variables with in-memory sessions.
To integrate your own auth system (JWT, OAuth, SSO, etc.), edit `lib/auth.ts`. The application only uses three functions:
- `checkAuth()` — returns `{ authenticated: boolean; email?: string }`
- `login(email, password)` — authenticates and sets session
- `logout()` — clears session
Replace these functions with your own implementation. The middleware in `middleware.ts` checks for a `session` cookie — update this if your auth uses a different mechanism (e.g., Bearer tokens).
## Development
```bash
npm run dev # Start dev server (port 3001)
npm run build # Production build
npm start # Start production server
npm run lint # Run ESLint
npm run setup # Initialize database
```
```
**Step 2: Create OPSÆTNING.md**
Write the Danish version with the same structure and content, translated to Danish.
**Step 3: Commit**
```bash
git add SETUP.md OPSÆTNING.md
git commit -m "docs: add setup guides in English and Danish"
```
---
### Task 11: Create scripts/build-release.sh
**Files:**
- Create: `scripts/build-release.sh`
**Step 1: Create the build script**
```bash
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
DATE=$(date +%Y-%m-%d)
RELEASE_DIR="/tmp/foamking-release-${TIMESTAMP}"
ZIP_NAME="foamking-beregner-${DATE}.zip"
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo "FoamKing Release Builder"
echo "========================"
echo ""
# Create temp directory
mkdir -p "${RELEASE_DIR}"
echo "Build directory: ${RELEASE_DIR}"
# Copy shipping files
echo "Copying files..."
cp -r "${PROJECT_DIR}/app" "${RELEASE_DIR}/"
cp -r "${PROJECT_DIR}/components" "${RELEASE_DIR}/"
cp -r "${PROJECT_DIR}/lib" "${RELEASE_DIR}/"
cp -r "${PROJECT_DIR}/public" "${RELEASE_DIR}/"
mkdir -p "${RELEASE_DIR}/scripts"
cp "${PROJECT_DIR}/scripts/setup.js" "${RELEASE_DIR}/scripts/"
cp "${PROJECT_DIR}/middleware.ts" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/package.json" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/tsconfig.json" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/tailwind.config.ts" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/postcss.config.mjs" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/eslint.config.mjs" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/next.config.ts" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/.env.example" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/SETUP.md" "${RELEASE_DIR}/"
cp "${PROJECT_DIR}/OPSÆTNING.md" "${RELEASE_DIR}/"
# Copy .prettierrc if it exists
[ -f "${PROJECT_DIR}/.prettierrc.json" ] && cp "${PROJECT_DIR}/.prettierrc.json" "${RELEASE_DIR}/"
# Verify build
echo ""
echo "Installing dependencies..."
cd "${RELEASE_DIR}"
npm install --ignore-scripts 2>&1 | tail -1
echo "Verifying build..."
npm run build 2>&1 | tail -3
# Clean up build artifacts
echo ""
echo "Cleaning up..."
rm -rf "${RELEASE_DIR}/node_modules"
rm -rf "${RELEASE_DIR}/.next"
# Create zip
cd /tmp
zip -rq "${ZIP_NAME}" "foamking-release-${TIMESTAMP}"
echo ""
echo "Release built successfully!"
echo "Zip: /tmp/${ZIP_NAME}"
echo "Size: $(du -h "/tmp/${ZIP_NAME}" | cut -f1)"
```
**Step 2: Make executable**
```bash
chmod +x scripts/build-release.sh
```
**Step 3: Test the build script**
```bash
./scripts/build-release.sh
```
Expected: Creates a zip at `/tmp/foamking-beregner-<date>.zip`
**Step 4: Commit**
```bash
git add scripts/build-release.sh
git commit -m "feat: add release build script for zip delivery"
```
---
### Task 12: Final verification — build, verify no secrets, test zip
**Step 1: Delete the existing data/quotes.db to test fresh setup**
```bash
rm -rf data/
```
**Step 2: Run the full flow**
```bash
npm run setup
npm run build
```
Expected: Both succeed.
**Step 3: Verify no secrets in tracked files**
Search for known credential patterns:
```bash
grep -r "Foamking1066\|sk-proj-\|eyJvcm" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.md" .
```
Expected: No matches (only in .env.local which is gitignored).
**Step 4: Run the build-release script**
```bash
./scripts/build-release.sh
```
Expected: Clean zip produced.
**Step 5: Final commit if any fixes needed, then tag**
```bash
git tag -a v1.0.0 -m "Release v1.0.0 - delivery-ready"
```