chore: remove password-lock; switch to ADMIN_TOKEN env; update docs
This commit is contained in:
14
.env.example
14
.env.example
@@ -1,12 +1,10 @@
|
|||||||
# Admin Credentials (REQUIRED)
|
# Admin Token (REQUIRED)
|
||||||
# Set a strong username and password for the admin account
|
# Long random string used to authenticate the admin via ?adm=<token> in the URL.
|
||||||
ADMIN_USERNAME=admin
|
# Generate with: openssl rand -hex 32
|
||||||
ADMIN_PASSWORD=changeme
|
ADMIN_TOKEN=replace-with-32-byte-random-hex
|
||||||
|
|
||||||
# JWT Secret (Optional - auto-generated if not provided)
|
# Public base URL used by the guest CLI when generating links
|
||||||
# For production, generate a secure random string:
|
PUBLIC_BASE_URL=http://localhost:3000
|
||||||
# openssl rand -base64 32
|
|
||||||
SECRET=
|
|
||||||
|
|
||||||
# Application Settings (Optional)
|
# Application Settings (Optional)
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|||||||
23
CLAUDE.md
23
CLAUDE.md
@@ -49,17 +49,28 @@ UI strings are hardcoded in the source. The PT-BR localization lives in:
|
|||||||
- `app/layout.tsx` — `<html lang="pt-BR">` and metadata description
|
- `app/layout.tsx` — `<html lang="pt-BR">` and metadata description
|
||||||
- `app/[slug]/page.tsx` — `Intl.NumberFormat('pt-BR', ...)` for prices
|
- `app/[slug]/page.tsx` — `Intl.NumberFormat('pt-BR', ...)` for prices
|
||||||
- `components/share-button.tsx` — "Compartilhar" label
|
- `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.
|
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=<token>` once. The token is set as an env var:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ADMIN_USERNAME=your_username
|
- ADMIN_TOKEN=long-random-hex-at-least-16-chars
|
||||||
- ADMIN_PASSWORD=your_password
|
```
|
||||||
|
|
||||||
|
The token is also accepted from the cookie `adm_token` after first visit.
|
||||||
|
|
||||||
|
### Guests
|
||||||
|
|
||||||
|
Each guest gets their own URL: `https://chadebebe.omeu.website/<slug>?usr=<token>`. Manage guests at `/admin/guests` or via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run guest:create -- --name="Martin"
|
||||||
|
npm run guest:list
|
||||||
|
npm run guest:delete -- --id=<guest-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
@@ -80,7 +91,7 @@ cd chadebebe
|
|||||||
docker compose up -d --build
|
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
|
### Updating the app
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db, settings } from '@/lib/db';
|
import { db, settings } from '@/lib/db';
|
||||||
import { verifyAdminToken } from '@/lib/auth/tokens';
|
import { verifyAdminToken } from '@/lib/auth/tokens';
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
// GET /api/settings - Get all settings (public endpoint for reading only)
|
// GET /api/settings - Get all settings (public endpoint for reading only)
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const allSettings = await db.select().from(settings);
|
const allSettings = await db.select().from(settings);
|
||||||
|
|
||||||
// Convert to key-value object
|
|
||||||
const settingsObj = allSettings.reduce((acc, setting) => {
|
const settingsObj = allSettings.reduce((acc, setting) => {
|
||||||
acc[setting.key] = setting.value;
|
acc[setting.key] = setting.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, string | boolean>);
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
// Set defaults if not found
|
|
||||||
if (!settingsObj.siteTitle) {
|
if (!settingsObj.siteTitle) {
|
||||||
settingsObj.siteTitle = 'Wishlist';
|
settingsObj.siteTitle = 'Wishlist';
|
||||||
}
|
}
|
||||||
@@ -23,12 +20,12 @@ export async function GET(request: NextRequest) {
|
|||||||
settingsObj.homepageSubtext = 'Browse and explore available wishlists';
|
settingsObj.homepageSubtext = 'Browse and explore available wishlists';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert passwordLockEnabled to boolean
|
|
||||||
(settingsObj as any).passwordLockEnabled = settingsObj.passwordLockEnabled === 'true';
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
settings: settingsObj,
|
settings: {
|
||||||
|
siteTitle: settingsObj.siteTitle,
|
||||||
|
homepageSubtext: settingsObj.homepageSubtext,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching settings:', error);
|
console.error('Error fetching settings:', error);
|
||||||
@@ -47,9 +44,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { siteTitle, homepageSubtext, passwordLockEnabled, passwordLock } = body;
|
const { siteTitle, homepageSubtext } = body;
|
||||||
|
|
||||||
// Update or insert siteTitle
|
|
||||||
if (siteTitle !== undefined) {
|
if (siteTitle !== undefined) {
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -70,7 +66,6 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update or insert homepageSubtext
|
|
||||||
if (homepageSubtext !== undefined) {
|
if (homepageSubtext !== undefined) {
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Settings updated successfully',
|
message: 'Settings updated successfully',
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ services:
|
|||||||
container_name: chadebebe
|
container_name: chadebebe
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
- ADMIN_TOKEN=${ADMIN_TOKEN:?ADMIN_TOKEN must be set}
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}
|
|
||||||
- PUID=${PUID:-1000}
|
- PUID=${PUID:-1000}
|
||||||
- PGID=${PGID:-1000}
|
- PGID=${PGID:-1000}
|
||||||
- COOKIE_SECURE=true
|
- COOKIE_SECURE=true
|
||||||
|
|||||||
Reference in New Issue
Block a user