mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: change theme
This commit is contained in:
parent
aca521b54f
commit
9426b73940
17 changed files with 326 additions and 21 deletions
|
|
@ -12,10 +12,14 @@ function escapeHtml(str) {
|
|||
}[c]));
|
||||
}
|
||||
|
||||
function establishSession(req, res, user, returnTo) {
|
||||
function establishSession(req, res, user, returnTo, idToken) {
|
||||
req.session.regenerate((err) => {
|
||||
if (err) throw err;
|
||||
req.session.userId = user.id;
|
||||
// Kept only so /auth/logout can end the Authentik SSO session too
|
||||
// (id_token_hint) — without it, logging out locally still leaves an
|
||||
// active Authentik session that silently re-authenticates the user.
|
||||
if (idToken) req.session.idToken = idToken;
|
||||
req.session.save((saveErr) => {
|
||||
if (saveErr) throw saveErr;
|
||||
res.redirect(returnTo || '/');
|
||||
|
|
@ -130,7 +134,10 @@ router.get(
|
|||
const user = await usersRepo.upsertFromClaims({
|
||||
sub: claims.sub,
|
||||
name: claims.name || claims.preferred_username || claims.email || claims.sub,
|
||||
username: claims.preferred_username || null,
|
||||
email: claims.email || null,
|
||||
avatarUrl: claims.picture || null,
|
||||
groups,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
|
|
@ -138,13 +145,36 @@ router.get(
|
|||
delete req.session.oidc;
|
||||
delete req.session.returnTo;
|
||||
|
||||
establishSession(req, res, user, returnTo);
|
||||
establishSession(req, res, user, returnTo, tokens.id_token);
|
||||
})
|
||||
);
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/');
|
||||
const idToken = req.session.idToken;
|
||||
|
||||
req.session.destroy((err) => {
|
||||
if (err) throw err;
|
||||
|
||||
// Local session is gone, but Authentik's own SSO session is still
|
||||
// active — redirecting straight to '/' would just have requireAuth
|
||||
// send the browser back through /auth/login, which Authentik would
|
||||
// silently re-approve (same user, no prompt). RP-initiated logout at
|
||||
// Authentik's end_session_endpoint is what actually signs them out.
|
||||
if (config.devBypassAuth || !idToken) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
try {
|
||||
const oidcConfig = getOidcConfig();
|
||||
const endSessionUrl = client.buildEndSessionUrl(oidcConfig, {
|
||||
id_token_hint: idToken,
|
||||
post_logout_redirect_uri: config.postLogoutRedirectUri,
|
||||
});
|
||||
res.redirect(endSessionUrl.href);
|
||||
} catch (endSessionErr) {
|
||||
console.warn('[auth] could not build Authentik end-session URL, falling back to local logout only:', endSessionErr.message);
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,5 +33,13 @@ module.exports = {
|
|||
oidcClientId: devBypassAuth ? process.env.OIDC_CLIENT_ID : required('OIDC_CLIENT_ID'),
|
||||
oidcClientSecret: devBypassAuth ? process.env.OIDC_CLIENT_SECRET : required('OIDC_CLIENT_SECRET'),
|
||||
oidcRedirectUri: devBypassAuth ? process.env.OIDC_REDIRECT_URI : required('OIDC_REDIRECT_URI'),
|
||||
// Where Authentik sends the browser back after RP-initiated logout (see
|
||||
// /auth/logout). Defaults to the app's own origin derived from
|
||||
// OIDC_REDIRECT_URI; override with POST_LOGOUT_REDIRECT_URI if needed.
|
||||
// Must be registered as an allowed logout redirect URI on the Authentik
|
||||
// provider, same as the regular redirect URI.
|
||||
postLogoutRedirectUri:
|
||||
process.env.POST_LOGOUT_REDIRECT_URI ||
|
||||
(process.env.OIDC_REDIRECT_URI ? new URL('/', process.env.OIDC_REDIRECT_URI).href : undefined),
|
||||
adminGroupName: process.env.ADMIN_GROUP_NAME || 'octane-admins',
|
||||
};
|
||||
|
|
|
|||
3
src/db/migrations/004_user_profile_fields.sql
Normal file
3
src/db/migrations/004_user_profile_fields.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE users ADD COLUMN IF NOT EXISTS username TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS groups TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
|
@ -1,23 +1,35 @@
|
|||
const pool = require('../db/pool');
|
||||
|
||||
const PROFILE_FIELDS = 'id, authentik_sub, name, username, email, avatar_url, groups, is_admin, created_at';
|
||||
|
||||
async function findById(id) {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, authentik_sub, name, email, is_admin, created_at FROM users WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
const { rows } = await pool.query(`SELECT ${PROFILE_FIELDS} FROM users WHERE id = $1`, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function upsertFromClaims({ sub, name, email, isAdmin }) {
|
||||
async function upsertFromClaims({ sub, name, username, email, avatarUrl, groups, isAdmin }) {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO users (authentik_sub, name, email, is_admin)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`INSERT INTO users (authentik_sub, name, username, email, avatar_url, groups, is_admin)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (authentik_sub)
|
||||
DO UPDATE SET name = $2, email = $3, is_admin = $4, updated_at = now()
|
||||
RETURNING id, authentik_sub, name, email, is_admin, created_at`,
|
||||
[sub, name, email, isAdmin]
|
||||
DO UPDATE SET name = $2, username = $3, email = $4, avatar_url = $5, groups = $6, is_admin = $7, updated_at = now()
|
||||
RETURNING ${PROFILE_FIELDS}`,
|
||||
[sub, name, username || null, email, avatarUrl || null, groups || [], isAdmin]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
module.exports = { findById, upsertFromClaims };
|
||||
async function getActivityStats(userId) {
|
||||
const [songs, suggestions, votes] = await Promise.all([
|
||||
pool.query('SELECT COUNT(*)::int AS count FROM songs WHERE added_by = $1', [userId]),
|
||||
pool.query('SELECT COUNT(*)::int AS count FROM suggestions WHERE suggested_by = $1', [userId]),
|
||||
pool.query('SELECT COUNT(*)::int AS count FROM suggestion_votes WHERE user_id = $1', [userId]),
|
||||
]);
|
||||
return {
|
||||
songsAdded: songs.rows[0].count,
|
||||
suggestionsProposed: suggestions.rows[0].count,
|
||||
votesCast: votes.rows[0].count,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { findById, upsertFromClaims, getActivityStats };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const express = require('express');
|
||||
const usersRepo = require('../repositories/usersRepo');
|
||||
const asyncHandler = require('../lib/asyncHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -6,8 +7,27 @@ const router = express.Router();
|
|||
router.get(
|
||||
'/me',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { id, name, email, is_admin } = req.user;
|
||||
res.json({ id, name, email, isAdmin: is_admin });
|
||||
const { id, name, username, email, avatar_url, is_admin } = req.user;
|
||||
res.json({ id, name, username, email, avatarUrl: avatar_url, isAdmin: is_admin });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/me/profile',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { id, name, username, email, avatar_url, groups, is_admin, created_at } = req.user;
|
||||
const stats = await usersRepo.getActivityStats(id);
|
||||
res.json({
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
avatarUrl: avatar_url,
|
||||
groups,
|
||||
isAdmin: is_admin,
|
||||
createdAt: created_at,
|
||||
stats,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue