addition of new dashboard
This commit is contained in:
199
backend/http/auth.js
Normal file
199
backend/http/auth.js
Normal file
@@ -0,0 +1,199 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user