From 25fc1a1b594243885bafd1d9b4d5074cbda4f9cd Mon Sep 17 00:00:00 2001 From: vegard Date: Wed, 18 Mar 2026 19:04:57 +0000 Subject: [PATCH] Username i auth_identities: Authentik-synk ved login (oppgave 26.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/auth.ts | 28 ++++++++++++++++++ maskinrommet/src/main.rs | 49 +++++++++++++++++++++++++++++++- migrations/027_auth_username.sql | 11 +++++++ tasks.md | 3 +- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 migrations/027_auth_username.sql diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index bfb3708..5400bc1 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -31,6 +31,29 @@ async function fetchNodeId(accessToken: string): Promise { 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 { + 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: [ @@ -69,6 +92,11 @@ export const { handle, signIn, signOut } = SvelteKitAuth({ 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; }, diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index ed37937..0f3f0fd 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -37,7 +37,7 @@ pub mod user_usage; mod workspace; use axum::{extract::State, http::StatusCode, middleware, routing::{get, post}, Json, Router}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -72,6 +72,16 @@ struct MeResponse { authentik_sub: String, } +#[derive(Deserialize)] +struct AuthSyncRequest { + username: String, +} + +#[derive(Serialize)] +struct AuthSyncResponse { + ok: bool, +} + #[tokio::main] async fn main() { // Strukturert logging: LOG_FORMAT=json for maskinlesbart, ellers human-readable. @@ -164,6 +174,7 @@ async fn main() { let app = Router::new() .route("/health", get(health)) .route("/me", get(me)) + .route("/auth/sync", post(auth_sync)) // WebSocket-endepunkt for sanntid via PG LISTEN/NOTIFY (oppgave 22.1) .route("/ws", get(ws::ws_handler)) .route("/intentions/create_node", post(intentions::create_node)) @@ -304,3 +315,39 @@ async fn me(user: AuthUser) -> Json { authentik_sub: user.authentik_sub, }) } + +/// POST /auth/sync — synkroniser brukerprofil fra Authentik ved login. +/// Kalles av SvelteKit auth-callback etter vellykket innlogging. +/// Oppdaterer username i auth_identities fra Authentik preferred_username. +async fn auth_sync( + user: AuthUser, + State(state): State, + Json(body): Json, +) -> Result, StatusCode> { + let username = body.username.trim().to_lowercase(); + + if username.is_empty() { + tracing::warn!("auth_sync: tomt brukernavn for node_id={}", user.node_id); + return Err(StatusCode::BAD_REQUEST); + } + + sqlx::query( + "UPDATE auth_identities SET username = $1 WHERE node_id = $2", + ) + .bind(&username) + .bind(user.node_id) + .execute(&state.db) + .await + .map_err(|e| { + tracing::error!("auth_sync: kunne ikke oppdatere username: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + tracing::info!( + "auth_sync: username='{}' for node_id={}", + username, + user.node_id + ); + + Ok(Json(AuthSyncResponse { ok: true })) +} diff --git a/migrations/027_auth_username.sql b/migrations/027_auth_username.sql new file mode 100644 index 0000000..314677b --- /dev/null +++ b/migrations/027_auth_username.sql @@ -0,0 +1,11 @@ +-- 027: Legg til username-kolonne i auth_identities +-- Populeres fra Authentik preferred_username ved login. +-- Brukes til epost-ruting (fase 26): vegard@synops.no → auth_identities.username = 'vegard' + +ALTER TABLE auth_identities +ADD COLUMN username TEXT UNIQUE; + +-- Sett Vegards username fra seed-data +UPDATE auth_identities +SET username = 'vegard' +WHERE email = 'vnotnes@pm.me'; diff --git a/tasks.md b/tasks.md index 2085e16..a6efd39 100644 --- a/tasks.md +++ b/tasks.md @@ -345,8 +345,7 @@ Vaktmesteren kan sende epost (msmtp) og motta epost (Postfix → synops-mail). Brukernavn@domene ruter til brukerens innboks. Alle domener (synops.no, sidelinja.org, vegard.info) ruter til samme bruker basert på username. -- [~] 26.1 Username i auth_identities: legg til `username`-kolonne, populer fra Authentik `preferred_username` ved login. Unik constraint. Oppdater auth-callback i SvelteKit til å lagre username. - > Påbegynt: 2026-03-18T19:00 +- [x] 26.1 Username i auth_identities: legg til `username`-kolonne, populer fra Authentik `preferred_username` ved login. Unik constraint. Oppdater auth-callback i SvelteKit til å lagre username. - [ ] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to --subject ` CLI-verktøy. - [ ] 26.3 MX-records: sett opp MX for synops.no, sidelinja.org, vegard.info som peker til serveren. - [ ] 26.4 Postfix minimal: installer Postfix som lokal MTA kun for mottak. Ingen relay, ingen kø for utgående. Pipe innkommende epost til `synops-mail --receive`.