diff --git a/.env.example b/.env.example index 83b7420..61deda1 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,10 @@ -# Admin Credentials (REQUIRED) -# Set a strong username and password for the admin account -ADMIN_USERNAME=admin -ADMIN_PASSWORD=changeme +# Admin Token (REQUIRED) +# Long random string used to authenticate the admin via ?adm= in the URL. +# Generate with: openssl rand -hex 32 +ADMIN_TOKEN=replace-with-32-byte-random-hex -# JWT Secret (Optional - auto-generated if not provided) -# For production, generate a secure random string: -# openssl rand -base64 32 -SECRET= +# Public base URL used by the guest CLI when generating links +PUBLIC_BASE_URL=http://localhost:3000 # Application Settings (Optional) NODE_ENV=production diff --git a/CLAUDE.md b/CLAUDE.md index 5a153db..4d01de5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,17 +49,28 @@ UI strings are hardcoded in the source. The PT-BR localization lives in: - `app/layout.tsx` — `` and metadata description - `app/[slug]/page.tsx` — `Intl.NumberFormat('pt-BR', ...)` for prices - `components/share-button.tsx` — "Compartilhar" label -- `app/lock/page.tsx`, `app/not-found.tsx`, `app/page.tsx` — page copy +- `app/not-found.tsx`, `app/page.tsx` — page copy To switch language, update those files and rebuild the image. -### Admin credentials +### Admin token -Set via environment variables in `docker-compose.yml`: +The admin authenticates by visiting the site with `?adm=` once. The token is set as an env var: ```yaml -- ADMIN_USERNAME=your_username -- ADMIN_PASSWORD=your_password +- ADMIN_TOKEN=long-random-hex-at-least-16-chars +``` + +The token is also accepted from the cookie `adm_token` after first visit. + +### Guests + +Each guest gets their own URL: `https://chadebebe.omeu.website/?usr=`. Manage guests at `/admin/guests` or via CLI: + +```bash +npm run guest:create -- --name="Martin" +npm run guest:list +npm run guest:delete -- --id= ``` ## Deployment @@ -80,7 +91,7 @@ cd chadebebe docker compose up -d --build ``` -The `data/db/wishlist.db` snapshot committed in the repo will be used as the initial database. On first run the app writes `data/secrets.json` (JWT keys) — this file is gitignored and must not be committed. +The `data/db/wishlist.db` snapshot committed in the repo will be used as the initial database. ### Updating the app diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 1170410..770c4d8 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -2,20 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, settings } from '@/lib/db'; import { verifyAdminToken } from '@/lib/auth/tokens'; -import crypto from 'crypto'; // GET /api/settings - Get all settings (public endpoint for reading only) -export async function GET(request: NextRequest) { +export async function GET() { try { const allSettings = await db.select().from(settings); - // Convert to key-value object const settingsObj = allSettings.reduce((acc, setting) => { acc[setting.key] = setting.value; return acc; - }, {} as Record); + }, {} as Record); - // Set defaults if not found if (!settingsObj.siteTitle) { settingsObj.siteTitle = 'Wishlist'; } @@ -23,12 +20,12 @@ export async function GET(request: NextRequest) { settingsObj.homepageSubtext = 'Browse and explore available wishlists'; } - // Convert passwordLockEnabled to boolean - (settingsObj as any).passwordLockEnabled = settingsObj.passwordLockEnabled === 'true'; - return NextResponse.json({ success: true, - settings: settingsObj, + settings: { + siteTitle: settingsObj.siteTitle, + homepageSubtext: settingsObj.homepageSubtext, + }, }); } catch (error) { console.error('Error fetching settings:', error); @@ -47,9 +44,8 @@ export async function PUT(request: NextRequest) { } const body = await request.json(); - const { siteTitle, homepageSubtext, passwordLockEnabled, passwordLock } = body; + const { siteTitle, homepageSubtext } = body; - // Update or insert siteTitle if (siteTitle !== undefined) { const existing = await db .select() @@ -70,7 +66,6 @@ export async function PUT(request: NextRequest) { } } - // Update or insert homepageSubtext if (homepageSubtext !== undefined) { const existing = await db .select() @@ -91,52 +86,6 @@ export async function PUT(request: NextRequest) { } } - // Update or insert passwordLockEnabled - if (passwordLockEnabled !== undefined) { - const value = passwordLockEnabled ? 'true' : 'false'; - const existing = await db - .select() - .from(settings) - .where(eq(settings.key, 'passwordLockEnabled')) - .limit(1); - - if (existing.length > 0) { - await db - .update(settings) - .set({ value, updatedAt: new Date() }) - .where(eq(settings.key, 'passwordLockEnabled')); - } else { - await db.insert(settings).values({ - key: 'passwordLockEnabled', - value, - }); - } - } - - // Update password hash if provided - if (passwordLock && passwordLock.trim() !== '') { - // Hash the password using SHA-256 - const hash = crypto.createHash('sha256').update(passwordLock).digest('hex'); - - const existing = await db - .select() - .from(settings) - .where(eq(settings.key, 'passwordLockHash')) - .limit(1); - - if (existing.length > 0) { - await db - .update(settings) - .set({ value: hash, updatedAt: new Date() }) - .where(eq(settings.key, 'passwordLockHash')); - } else { - await db.insert(settings).values({ - key: 'passwordLockHash', - value: hash, - }); - } - } - return NextResponse.json({ success: true, message: 'Settings updated successfully', diff --git a/docker-compose.yml b/docker-compose.yml index 006304b..10d2bea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,7 @@ services: container_name: chadebebe restart: unless-stopped environment: - - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - - ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme} + - ADMIN_TOKEN=${ADMIN_TOKEN:?ADMIN_TOKEN must be set} - PUID=${PUID:-1000} - PGID=${PGID:-1000} - COOKIE_SECURE=true