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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
// blocklist
|
||||||
await query(`
|
await query(`
|
||||||
CREATE TABLE IF NOT EXISTS blocklist (
|
CREATE TABLE IF NOT EXISTS blocklist (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { query } = require('../db');
|
|||||||
|
|
||||||
function signToken(user) {
|
function signToken(user) {
|
||||||
return jwt.sign(
|
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,
|
process.env.JWT_SECRET,
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
{ 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' });
|
return res.status(400).json({ error: 'email and password are required' });
|
||||||
|
|
||||||
const result = await query(
|
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()]
|
[email.toLowerCase().trim()]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,7 +81,7 @@ router.post('/login', async (req, res, next) => {
|
|||||||
return res.status(401).json({ error: 'Invalid credentials' });
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
|
||||||
const { password_hash: _, is_active: __, ...safeUser } = user;
|
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); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ const ALLOWED_BLOCKED_REASONS = ['regenerate', 'not_to_use'];
|
|||||||
// GET /api/pictures
|
// GET /api/pictures
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
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 params = [Math.min(parseInt(limit), 500), parseInt(offset)];
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
if (status) { conditions.push(`status = $${params.length + 1}`); params.push(status); }
|
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 (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 where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`SELECT * FROM pictures ${where} ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
`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 = [
|
const allowed = [
|
||||||
'status', 'blocked_reason', 'generation_prompt', 'generation_timestamp',
|
'status', 'blocked_reason', 'generation_prompt', 'generation_timestamp',
|
||||||
'generation_duration_s', 'published_timestamp', 'blocked_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));
|
const fields = Object.keys(req.body).filter(k => allowed.includes(k));
|
||||||
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });
|
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))
|
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(', ')}` });
|
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
|
// Auto-timestamps bei Statuswechsel
|
||||||
if (req.body.status === 'published' && !req.body.published_timestamp)
|
if (req.body.status === 'published' && !req.body.published_timestamp)
|
||||||
fields.push('published_timestamp'), req.body.published_timestamp = new Date().toISOString();
|
fields.push('published_timestamp'), req.body.published_timestamp = new Date().toISOString();
|
||||||
|
|||||||
Reference in New Issue
Block a user