Username i auth_identities: Authentik-synk ved login (oppgave 26.1)

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.
This commit is contained in:
vegard 2026-03-18 19:04:57 +00:00
parent 6b81569581
commit 25fc1a1b59
4 changed files with 88 additions and 3 deletions

View file

@ -31,6 +31,29 @@ async function fetchNodeId(accessToken: string): Promise<string | null> {
return null; 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({ export const { handle, signIn, signOut } = SvelteKitAuth({
trustHost: true, trustHost: true,
providers: [ providers: [
@ -69,6 +92,11 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
if (!token.node_id) { if (!token.node_id) {
token.node_id = await fetchNodeId(account.access_token); 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; return token;
}, },

View file

@ -37,7 +37,7 @@ pub mod user_usage;
mod workspace; mod workspace;
use axum::{extract::State, http::StatusCode, middleware, routing::{get, post}, Json, Router}; 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::postgres::PgPoolOptions;
use sqlx::PgPool; use sqlx::PgPool;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
@ -72,6 +72,16 @@ struct MeResponse {
authentik_sub: String, authentik_sub: String,
} }
#[derive(Deserialize)]
struct AuthSyncRequest {
username: String,
}
#[derive(Serialize)]
struct AuthSyncResponse {
ok: bool,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Strukturert logging: LOG_FORMAT=json for maskinlesbart, ellers human-readable. // Strukturert logging: LOG_FORMAT=json for maskinlesbart, ellers human-readable.
@ -164,6 +174,7 @@ async fn main() {
let app = Router::new() let app = Router::new()
.route("/health", get(health)) .route("/health", get(health))
.route("/me", get(me)) .route("/me", get(me))
.route("/auth/sync", post(auth_sync))
// WebSocket-endepunkt for sanntid via PG LISTEN/NOTIFY (oppgave 22.1) // WebSocket-endepunkt for sanntid via PG LISTEN/NOTIFY (oppgave 22.1)
.route("/ws", get(ws::ws_handler)) .route("/ws", get(ws::ws_handler))
.route("/intentions/create_node", post(intentions::create_node)) .route("/intentions/create_node", post(intentions::create_node))
@ -304,3 +315,39 @@ async fn me(user: AuthUser) -> Json<MeResponse> {
authentik_sub: user.authentik_sub, 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<AppState>,
Json(body): Json<AuthSyncRequest>,
) -> Result<Json<AuthSyncResponse>, 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 }))
}

View file

@ -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';

View file

@ -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, Brukernavn@domene ruter til brukerens innboks. Alle domener (synops.no,
sidelinja.org, vegard.info) ruter til samme bruker basert på username. 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. - [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.
> Påbegynt: 2026-03-18T19:00
- [ ] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to <epost> --subject <emne>` CLI-verktøy. - [ ] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to <epost> --subject <emne>` CLI-verktøy.
- [ ] 26.3 MX-records: sett opp MX for synops.no, sidelinja.org, vegard.info som peker til serveren. - [ ] 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`. - [ ] 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`.