addition of new dashboard

This commit is contained in:
2026-08-20 15:08:32 +00:00
parent 0622018d95
commit bf59249ed7
161 changed files with 13170 additions and 119 deletions

199
backend/http/auth.js Normal file
View 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);
}
}