From 5411e478cbba3d05040757d2cc85ef6b8f1cab26 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 24 May 2026 19:20:43 +0200 Subject: [PATCH] feat: objects_created flag on pictures, native_lang on users - pictures: add objects_created (bool) + objects_created_at (auto timestamp) GET /pictures supports ?objects_created=true/false filter PATCH /pictures/:id allows setting objects_created - db-migrate: seed German language, link to all existing users - auth/login: include native_lang (from languages table) in response + JWT Co-Authored-By: Claude Sonnet 4.6 --- src/db-migrate.js | 20 ++++++++++++++++++++ src/routes/auth.js | 10 +++++++--- src/routes/pictures.js | 13 +++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/db-migrate.js b/src/db-migrate.js index 227b23e..e6a0264 100644 --- a/src/db-migrate.js +++ b/src/db-migrate.js @@ -319,6 +319,26 @@ async function migrate() { ) `); + // objects_created on pictures + await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS objects_created BOOLEAN NOT NULL DEFAULT false`); + await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS objects_created_at TIMESTAMPTZ`); + + // language_native_id on users + await query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS language_native_id UUID REFERENCES languages(id) ON DELETE SET NULL`); + + // Seed German language (idempotent) + await query(` + INSERT INTO languages (titel_en, titel_de, titel_sv, short_en, status) + SELECT 'German', 'Deutsch', 'Tyska', 'de', 'published' + WHERE NOT EXISTS (SELECT 1 FROM languages WHERE short_en = 'de') + `); + + // Set all users without a native language to German + await query(` + UPDATE users SET language_native_id = (SELECT id FROM languages WHERE short_en = 'de' LIMIT 1) + WHERE language_native_id IS NULL + `); + // blocklist await query(` CREATE TABLE IF NOT EXISTS blocklist ( diff --git a/src/routes/auth.js b/src/routes/auth.js index 2f57ca0..fc94fdb 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -5,7 +5,7 @@ const { query } = require('../db'); function signToken(user) { return jwt.sign( - { userId: user.id, email: user.email, role: user.role }, + { userId: user.id, email: user.email, role: user.role, native_lang: user.native_lang || 'de' }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } ); @@ -60,7 +60,11 @@ router.post('/login', async (req, res, next) => { return res.status(400).json({ error: 'email and password are required' }); const result = await query( - `SELECT id, email, role, password_hash, is_active FROM users WHERE lower(email) = $1`, + `SELECT u.id, u.email, u.role, u.password_hash, u.is_active, + COALESCE(l.short_en, 'de') AS native_lang + FROM users u + LEFT JOIN languages l ON l.id = u.language_native_id + WHERE lower(u.email) = $1`, [email.toLowerCase().trim()] ); @@ -77,7 +81,7 @@ router.post('/login', async (req, res, next) => { return res.status(401).json({ error: 'Invalid credentials' }); const { password_hash: _, is_active: __, ...safeUser } = user; - res.json({ user: safeUser, token: signToken(safeUser) }); + res.json({ user: safeUser, token: signToken(safeUser), native_lang: safeUser.native_lang }); } catch (err) { next(err); } }); diff --git a/src/routes/pictures.js b/src/routes/pictures.js index d6917d9..ce7833b 100644 --- a/src/routes/pictures.js +++ b/src/routes/pictures.js @@ -12,11 +12,13 @@ const ALLOWED_BLOCKED_REASONS = ['regenerate', 'not_to_use']; // GET /api/pictures router.get('/', async (req, res, next) => { try { - const { status, search, limit = 50, offset = 0 } = req.query; + const { status, search, objects_created, limit = 50, offset = 0 } = req.query; const params = [Math.min(parseInt(limit), 500), parseInt(offset)]; const conditions = []; if (status) { conditions.push(`status = $${params.length + 1}`); params.push(status); } if (search) { conditions.push(`lower(design) LIKE $${params.length + 1}`); params.push(`%${search.toLowerCase()}%`); } + if (objects_created === 'true') { conditions.push(`objects_created = true`); } + if (objects_created === 'false') { conditions.push(`(objects_created IS NULL OR objects_created = false)`); } const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await query( `SELECT * FROM pictures ${where} ORDER BY created_at DESC LIMIT $1 OFFSET $2`, @@ -117,7 +119,8 @@ router.patch('/:id', async (req, res, next) => { const allowed = [ 'status', 'blocked_reason', 'generation_prompt', 'generation_timestamp', 'generation_duration_s', 'published_timestamp', 'blocked_timestamp', - 'blurhash', 'picture_link', 'design' + 'blurhash', 'picture_link', 'design', + 'objects_created', 'objects_created_at', ]; const fields = Object.keys(req.body).filter(k => allowed.includes(k)); if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' }); @@ -127,6 +130,12 @@ router.patch('/:id', async (req, res, next) => { if (req.body.blocked_reason && !ALLOWED_BLOCKED_REASONS.includes(req.body.blocked_reason)) return res.status(400).json({ error: `blocked_reason must be one of: ${ALLOWED_BLOCKED_REASONS.join(', ')}` }); + // Auto-timestamp when objects_created flipped to true + if (req.body.objects_created === true && !req.body.objects_created_at) { + fields.push('objects_created_at'); + req.body.objects_created_at = new Date().toISOString(); + } + // Auto-timestamps bei Statuswechsel if (req.body.status === 'published' && !req.body.published_timestamp) fields.push('published_timestamp'), req.body.published_timestamp = new Date().toISOString();