Legger til username-kolonne i auth_identities med UNIQUE constraint. Ved innlogging sender SvelteKit preferred_username fra Authentik til maskinrommet POST /auth/sync, som oppdaterer kolonnen. Grunnlaget for epost-ruting i fase 26: vegard@synops.no → username-oppslag.
116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import { SvelteKitAuth } from '@auth/sveltekit';
|
|
import type { OIDCConfig } from '@auth/core/providers';
|
|
import { env } from '$env/dynamic/private';
|
|
|
|
interface AuthentikProfile {
|
|
sub: string;
|
|
email: string;
|
|
name: string;
|
|
preferred_username: string;
|
|
groups: string[];
|
|
}
|
|
|
|
/**
|
|
* Fetch the user's node_id from maskinrommet using the Authentik access token.
|
|
* Called once at sign-in so the node_id is cached in the JWT for future requests.
|
|
*/
|
|
async function fetchNodeId(accessToken: string): Promise<string | null> {
|
|
const url = env.MASKINROMMET_URL ?? 'https://api.sidelinja.org';
|
|
try {
|
|
const res = await fetch(`${url}/me`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
return data.node_id ?? null;
|
|
}
|
|
console.error(`[auth] /me returned ${res.status}: ${await res.text()}`);
|
|
} catch (e) {
|
|
console.error('[auth] Failed to fetch node_id from maskinrommet:', e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sync user profile (username) to maskinrommet on sign-in.
|
|
* Stores Authentik preferred_username in auth_identities.username.
|
|
*/
|
|
async function syncUsername(accessToken: string, username: string): Promise<void> {
|
|
const url = env.MASKINROMMET_URL ?? 'https://api.sidelinja.org';
|
|
try {
|
|
const res = await fetch(`${url}/auth/sync`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ username })
|
|
});
|
|
if (!res.ok) {
|
|
console.error(`[auth] /auth/sync returned ${res.status}: ${await res.text()}`);
|
|
}
|
|
} catch (e) {
|
|
console.error('[auth] Failed to sync username to maskinrommet:', e);
|
|
}
|
|
}
|
|
|
|
export const { handle, signIn, signOut } = SvelteKitAuth({
|
|
trustHost: true,
|
|
providers: [
|
|
{
|
|
id: 'authentik',
|
|
name: 'Authentik',
|
|
type: 'oidc',
|
|
issuer: env.AUTHENTIK_ISSUER,
|
|
clientId: env.AUTHENTIK_CLIENT_ID,
|
|
clientSecret: env.AUTHENTIK_CLIENT_SECRET,
|
|
authorization: {
|
|
params: {
|
|
scope: 'openid email profile offline_access'
|
|
}
|
|
},
|
|
checks: ['pkce', 'state'],
|
|
profile(profile: AuthentikProfile) {
|
|
return {
|
|
id: profile.sub,
|
|
name: profile.name ?? profile.preferred_username,
|
|
email: profile.email
|
|
};
|
|
}
|
|
} satisfies OIDCConfig<AuthentikProfile>
|
|
],
|
|
callbacks: {
|
|
async jwt({ token, account, profile }) {
|
|
// profile is only available on initial sign-in, not on refresh.
|
|
// Store authentik_sub in the token so it persists across refreshes.
|
|
if (profile?.sub) {
|
|
token.authentik_sub = profile.sub;
|
|
}
|
|
// On initial sign-in, fetch node_id and store access_token
|
|
if (account?.access_token) {
|
|
token.access_token = account.access_token;
|
|
if (!token.node_id) {
|
|
token.node_id = await fetchNodeId(account.access_token);
|
|
}
|
|
// Sync username from Authentik preferred_username on each sign-in
|
|
const p = profile as AuthentikProfile | undefined;
|
|
if (p?.preferred_username) {
|
|
await syncUsername(account.access_token, p.preferred_username);
|
|
}
|
|
}
|
|
return token;
|
|
},
|
|
session({ session, token }) {
|
|
if (session.user) {
|
|
// Use Authentik sub as user ID (not @auth/sveltekit's internal ID)
|
|
session.user.id = (token.authentik_sub ?? token.sub) as string;
|
|
}
|
|
// Expose node_id and access_token so frontend can call maskinrommet
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const s = session as any;
|
|
s.nodeId = token.node_id as string | undefined;
|
|
s.accessToken = token.access_token as string | undefined;
|
|
return session;
|
|
}
|
|
}
|
|
});
|