synops/frontend/src/auth.ts
vegard daaafd34ab TipTap-editor med create_node-intensjon (oppgave 3.5)
Legger til en TipTap-basert rik tekst-editor i mottaksflaten som sender
create_node-intensjoner til maskinrommet ved submit.

- TipTap med StarterKit (tekst, markdown), Link-extension og Placeholder
- NodeEditor.svelte: tittel + innhold, Ctrl+Enter for submit
- API-klient (lib/api.ts) som kaller maskinrommet via /api proxy
- Authentik access_token eksponert i session for API-kall
- Vite proxy rewrite fikset (/api → maskinrommet root)
- HTML-innhold lagres i metadata.document, ren tekst i content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:24:25 +01:00

88 lines
2.6 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;
}
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);
}
}
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;
}
}
});