199 lines
6.3 KiB
JavaScript
199 lines
6.3 KiB
JavaScript
import { getConfig } from "../common/config.js";
|
|
import { getPool } from "../db/index.js";
|
|
import { verifyKey } from "../modules/core/apiKey.js";
|
|
import crypto from 'crypto';
|
|
|
|
// Bearer-token auth for the central /api/v1/* API. On success it sets
|
|
// req.session.user_id (which the core router scopes queries by). It does NOT
|
|
// populate req.user — the browser session routes (passport) own that — so the
|
|
// session-authed dashboard/key-management routes remain login-only by design.
|
|
export async function apiMiddleware(req, res, next) {
|
|
let success = false;
|
|
try {
|
|
const apiKey = req.headers['authorization']?.split(' ')[1];
|
|
if (apiKey) {
|
|
// Preferred: the hashed multi-key store (with a short in-memory cache).
|
|
const found = await verifyKey(apiKey);
|
|
if (found) {
|
|
req.session.user_id = found.user_id;
|
|
success = true;
|
|
} else {
|
|
// Fallback: the legacy single plaintext key in `auth` (issued at login).
|
|
const client = await getPool();
|
|
try {
|
|
const r = await client.query(
|
|
`SELECT au.user_id FROM public.auth au WHERE au.api_key = $1`,
|
|
[apiKey],
|
|
);
|
|
if (r.rowCount > 0) {
|
|
req.session.user_id = r.rows[0].user_id;
|
|
success = true;
|
|
}
|
|
} finally {
|
|
await client.release();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
if (!success)
|
|
return res.status(401).json({ error: 'API key is invalid' });
|
|
next();
|
|
}
|
|
|
|
export function getNewKey(user) {
|
|
const seed = `${user.issuer}:${user.identifier}:${user.id}:${crypto.randomBytes(16).toString('hex')}`;
|
|
const hash = crypto.createHash('sha256').update(seed).digest();
|
|
const apiKey = hash.toString('hex');
|
|
return apiKey;
|
|
}
|
|
|
|
async function createApiKeys(client, user, issuer) {
|
|
const config = getConfig();
|
|
for (const type in config.appModules) {
|
|
// Internal modules (e.g. dashboard) have no `app` row and mint no keys.
|
|
if (config.appModules[type]?.internal) continue;
|
|
const apiKey = issuer !== 'https://single'
|
|
? getNewKey(user)
|
|
: config.authModules.single[type]
|
|
await client.query(`
|
|
INSERT INTO public.auth (
|
|
user_id,
|
|
app_id,
|
|
api_key
|
|
) VALUES (
|
|
$1,
|
|
(SELECT id FROM public.app WHERE type = $2),
|
|
$3
|
|
) ON CONFLICT DO NOTHING;
|
|
`, [
|
|
user.id,
|
|
type,
|
|
apiKey,
|
|
]);
|
|
}
|
|
}
|
|
|
|
async function checkIfUserExists(client, issuer, identifier) {
|
|
return await client.query(`
|
|
SELECT
|
|
u.id,
|
|
u.photo,
|
|
u.username
|
|
FROM public.user u
|
|
WHERE u.identifier = $1
|
|
AND u.issuer = $2
|
|
`,
|
|
[
|
|
identifier,
|
|
issuer,
|
|
]);
|
|
}
|
|
|
|
export async function getUser(client, issuer, identifier) {
|
|
const res = await client.query(`
|
|
SELECT
|
|
u.id,
|
|
u.username,
|
|
u.photo,
|
|
u.issuer,
|
|
u.identifier,
|
|
COALESCE(ut.can_view_json, FALSE) AS "canViewJson",
|
|
json_agg(
|
|
jsonb_build_object(
|
|
'id', a.id,
|
|
'type', a.type,
|
|
'apiKey', au.api_key
|
|
)
|
|
) AS apps
|
|
FROM public.user u
|
|
INNER JOIN public.auth au ON u.id = au.user_id
|
|
INNER JOIN public.app a ON au.app_id = a.id
|
|
LEFT JOIN public.user_type ut ON ut.id = u.user_type_id
|
|
WHERE u.identifier = $1
|
|
AND u.issuer = $2
|
|
GROUP BY
|
|
u.id,
|
|
u.username,
|
|
u.photo,
|
|
u.issuer,
|
|
u.identifier,
|
|
ut.can_view_json
|
|
`,
|
|
[
|
|
identifier,
|
|
issuer,
|
|
]);
|
|
if (res.rowCount > 0) {
|
|
const user = res.rows[0];
|
|
return user;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function checkUser(type, issuer, identifier, username, photo) {
|
|
const client = await getPool();
|
|
let userExists = await checkIfUserExists(client, issuer, identifier);
|
|
if (userExists.rowCount === 0) {
|
|
if (!username || username === '') {
|
|
username = identifier;
|
|
}
|
|
// New users default to the least-privileged role; an administrator
|
|
// promotes them. Existing users keep whatever the DB already holds
|
|
// (see the 000019 backfill) — checkUser never overwrites an existing type.
|
|
const res = await client.query(`
|
|
INSERT INTO public.user (
|
|
username,
|
|
issuer,
|
|
identifier,
|
|
photo,
|
|
type,
|
|
user_type_id
|
|
) VALUES (
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
$5,
|
|
(SELECT id FROM public.user_type WHERE value = 'standard')
|
|
) RETURNING id;
|
|
`, [
|
|
username,
|
|
issuer,
|
|
identifier,
|
|
photo,
|
|
type,
|
|
]);
|
|
if (res.rowCount === 0) {
|
|
return null;
|
|
}
|
|
await createApiKeys(client, res.rows[0], issuer);
|
|
} else {
|
|
if (photo !== userExists.rows[0].photo || username != userExists.rows[0].username) {
|
|
await client.query(`
|
|
UPDATE public.user SET
|
|
photo = $1,
|
|
username = $2,
|
|
updated_ts = NOW()
|
|
WHERE id = $3
|
|
`, [
|
|
userExists.rows[0].photo,
|
|
userExists.rows[0].username,
|
|
userExists.rows[0].id,
|
|
]);
|
|
}
|
|
await createApiKeys(client, userExists.rows[0], issuer);
|
|
}
|
|
const user = await getUser(client, issuer, identifier);
|
|
return user;
|
|
}
|
|
|
|
export async function initModules(app, passport) {
|
|
const config = getConfig();
|
|
for (const authModule of Object.keys(config.authModules)) {
|
|
const authModulePath = `../modules/auth/${authModule}.js`;
|
|
const { initModule } = await import(authModulePath);
|
|
await initModule(app, passport);
|
|
}
|
|
} |