add api key for uploading via 3rd party tools
This commit is contained in:
@@ -334,6 +334,67 @@ export default new class {
|
||||
return next();
|
||||
};
|
||||
|
||||
// Middleware: authenticate via X-Api-Key header (upload-only)
|
||||
async apiKeyAuth(req, res, next) {
|
||||
const key = req.headers['x-api-key'];
|
||||
if (!key) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: JSON.stringify({ success: false, msg: 'API key required' }),
|
||||
type: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
let row;
|
||||
try {
|
||||
const rows = await db`
|
||||
SELECT
|
||||
u.id, u.user, u.login, u.admin, u.is_moderator, u.banned,
|
||||
uo.display_name, uo.mode, uo.theme, uo.avatar, uo.avatar_file,
|
||||
uo.username_color, uo.show_motd, uo.disable_autoplay,
|
||||
uo.disable_swiping, uo.use_new_layout, uo.excluded_tags,
|
||||
uo.ruffle_background, uo.ruffle_volume, uo.quote_emojis,
|
||||
uo.embed_youtube_in_comments, uo.hide_koepfe,
|
||||
uo.use_alternative_infobox, uo.language, uo.comment_display_mode,
|
||||
uo.force_comment_display_mode, uo.min_xd_score, uo.show_background,
|
||||
uo.font, uo.receive_system_notifications, uo.receive_user_notifications,
|
||||
uo.do_not_disturb, uo.description
|
||||
FROM user_api_keys k
|
||||
JOIN "user" u ON u.id = k.user_id
|
||||
LEFT JOIN user_options uo ON uo.user_id = u.id
|
||||
WHERE k.api_key = ${key}
|
||||
LIMIT 1
|
||||
`;
|
||||
row = rows[0];
|
||||
} catch (err) {
|
||||
console.error('[API KEY AUTH] DB error:', err);
|
||||
return res.reply({
|
||||
code: 500,
|
||||
body: JSON.stringify({ success: false, msg: 'Internal server error' }),
|
||||
type: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.reply({
|
||||
code: 401,
|
||||
body: JSON.stringify({ success: false, msg: 'Invalid API key' }),
|
||||
type: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
if (row.banned) {
|
||||
return res.reply({
|
||||
code: 403,
|
||||
body: JSON.stringify({ success: false, msg: 'Account banned' }),
|
||||
type: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
req.session = { ...row, api_key_auth: true };
|
||||
return next();
|
||||
};
|
||||
|
||||
getCookieOptions(expires = null, httpOnly = true) {
|
||||
const isSecure = cfg.main.url.full && cfg.main.url.full.startsWith('https');
|
||||
let options = "Path=/; SameSite=Lax";
|
||||
|
||||
@@ -3,6 +3,7 @@ import lib from '../../lib.mjs';
|
||||
import cfg from '../../config.mjs';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Note: Avatar upload/delete is handled by middleware in index.mjs via avatar_handler.mjs
|
||||
// These routes remain for other settings API endpoints
|
||||
@@ -726,6 +727,81 @@ export default router => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Upload API Key Management ---
|
||||
|
||||
// GET /api/v2/settings/api-key
|
||||
// Returns whether the user has an API key, when it was created, and the last 8 chars (masked preview).
|
||||
group.get(/\/api-key$/, lib.loggedin, async (req, res) => {
|
||||
try {
|
||||
const row = (await db`
|
||||
SELECT api_key, created_at
|
||||
FROM user_api_keys
|
||||
WHERE user_id = ${+req.session.id}
|
||||
LIMIT 1
|
||||
`)[0];
|
||||
|
||||
if (!row) {
|
||||
return res.json({ success: true, has_key: false }, 200);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
has_key: true,
|
||||
preview: `****${row.api_key.slice(-8)}`,
|
||||
created_at: row.created_at
|
||||
}, 200);
|
||||
} catch (e) {
|
||||
console.error('[API KEY] GET error:', e);
|
||||
return res.json({ success: false, msg: 'Error fetching API key' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v2/settings/api-key/regenerate
|
||||
// Generates a new key (or replaces an existing one). Returns the full key — only shown once.
|
||||
group.post(/\/api-key\/regenerate$/, lib.loggedin, async (req, res) => {
|
||||
try {
|
||||
const newKey = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
await db`
|
||||
INSERT INTO user_api_keys (user_id, api_key, created_at)
|
||||
VALUES (${+req.session.id}, ${newKey}, now())
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET api_key = EXCLUDED.api_key,
|
||||
created_at = now()
|
||||
`;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
api_key: newKey,
|
||||
msg: 'API key generated. Copy it now — it will not be shown again in full.'
|
||||
}, 200);
|
||||
} catch (e) {
|
||||
console.error('[API KEY] Regenerate error:', e);
|
||||
return res.json({ success: false, msg: 'Error generating API key' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/v2/settings/api-key
|
||||
// Revokes (deletes) the user's API key.
|
||||
group.delete(/\/api-key$/, lib.loggedin, async (req, res) => {
|
||||
try {
|
||||
const result = await db`
|
||||
DELETE FROM user_api_keys
|
||||
WHERE user_id = ${+req.session.id}
|
||||
RETURNING user_id
|
||||
`;
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.json({ success: false, msg: 'No API key to revoke' }, 404);
|
||||
}
|
||||
|
||||
return res.json({ success: true, msg: 'API key revoked' }, 200);
|
||||
} catch (e) {
|
||||
console.error('[API KEY] Delete error:', e);
|
||||
return res.json({ success: false, msg: 'Error revoking API key' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return group;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user