Files
cameleer-server/ui/src/auth/auth-store.ts
hsiegeln a6f94e8a70
Some checks failed
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 48s
CI / deploy (push) Has been cancelled
Full OIDC logout with id_token_hint for provider session termination
Return the OIDC id_token in the callback response so the frontend can
store it and pass it as id_token_hint to the provider's end-session
endpoint on logout. This lets Authentik (or any OIDC provider) honor
the post_logout_redirect_uri and redirect back to the Cameleer login
page instead of showing the provider's own logout page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:14:07 +01:00

166 lines
4.8 KiB
TypeScript

import { create } from 'zustand';
import { api } from '../api/client';
interface AuthState {
accessToken: string | null;
refreshToken: string | null;
username: string | null;
roles: string[];
isAuthenticated: boolean;
error: string | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
loginWithOidcCode: (code: string, redirectUri: string) => Promise<void>;
refresh: () => Promise<boolean>;
logout: () => void;
}
function parseRolesFromJwt(token: string): string[] {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return Array.isArray(payload.roles) ? payload.roles : [];
} catch {
return [];
}
}
function loadTokens() {
return {
accessToken: localStorage.getItem('cameleer-access-token'),
refreshToken: localStorage.getItem('cameleer-refresh-token'),
username: localStorage.getItem('cameleer-username'),
};
}
function persistTokens(access: string, refresh: string, username: string) {
localStorage.setItem('cameleer-access-token', access);
localStorage.setItem('cameleer-refresh-token', refresh);
localStorage.setItem('cameleer-username', username);
}
function clearTokens() {
localStorage.removeItem('cameleer-access-token');
localStorage.removeItem('cameleer-refresh-token');
localStorage.removeItem('cameleer-username');
}
const initial = loadTokens();
export const useAuthStore = create<AuthState>((set, get) => ({
accessToken: initial.accessToken,
refreshToken: initial.refreshToken,
username: initial.username,
roles: initial.accessToken ? parseRolesFromJwt(initial.accessToken) : [],
isAuthenticated: !!initial.accessToken,
error: null,
loading: false,
login: async (username, password) => {
set({ loading: true, error: null });
try {
const { data, error } = await api.POST('/auth/login', {
body: { username, password },
});
if (error || !data) {
throw new Error('Invalid credentials');
}
const { accessToken, refreshToken, displayName } = data;
localStorage.removeItem('cameleer-oidc-end-session');
localStorage.removeItem('cameleer-oidc-id-token');
const name = displayName ?? username;
persistTokens(accessToken, refreshToken, name);
set({
accessToken,
refreshToken,
username: name,
roles: parseRolesFromJwt(accessToken),
isAuthenticated: true,
loading: false,
});
} catch (e: unknown) {
set({
error: e instanceof Error ? e.message : 'Login failed',
loading: false,
});
}
},
loginWithOidcCode: async (code, redirectUri) => {
set({ loading: true, error: null });
try {
const { data, error } = await api.POST('/auth/oidc/callback', {
body: { code, redirectUri },
});
if (error || !data) {
throw new Error('OIDC login failed');
}
const { accessToken, refreshToken, displayName, idToken } = data;
const username = displayName ?? 'oidc-user';
persistTokens(accessToken, refreshToken, username);
if (idToken) {
localStorage.setItem('cameleer-oidc-id-token', idToken);
}
set({
accessToken,
refreshToken,
username,
roles: parseRolesFromJwt(accessToken),
isAuthenticated: true,
loading: false,
});
} catch (e: unknown) {
set({
error: e instanceof Error ? e.message : 'OIDC login failed',
loading: false,
});
}
},
refresh: async () => {
const { refreshToken } = get();
if (!refreshToken) return false;
try {
const { data, error } = await api.POST('/auth/refresh', {
body: { refreshToken },
});
if (error || !data) return false;
const username = data.displayName ?? get().username ?? '';
persistTokens(data.accessToken, data.refreshToken, username);
set({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
username,
roles: parseRolesFromJwt(data.accessToken),
isAuthenticated: true,
});
return true;
} catch {
return false;
}
},
logout: () => {
const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session');
const idToken = localStorage.getItem('cameleer-oidc-id-token');
clearTokens();
localStorage.removeItem('cameleer-oidc-end-session');
localStorage.removeItem('cameleer-oidc-id-token');
set({
accessToken: null,
refreshToken: null,
username: null,
roles: [],
isAuthenticated: false,
error: null,
});
if (endSessionEndpoint && idToken) {
const postLogoutRedirect = `${window.location.origin}/login`;
const params = new URLSearchParams({
id_token_hint: idToken,
post_logout_redirect_uri: postLogoutRedirect,
});
window.location.href = `${endSessionEndpoint}?${params}`;
}
},
}));