Backend (main.py + auth.py): - Validate Bearer JWT against the intense-sale realm JWKS (RS256, issuer checked, aud skipped). Missing/invalid token = anonymous, never an error, so the public map keeps working when Keycloak is down. - Anonymous truncation: /api/meta hides brand/manager/status facets; /api/bookings blanks brand/company_name/manager/task_id/task_status (bars stay, one neutral colour); /api/map keeps occupied_now but empties booking details. Hidden facet filters are ignored for anonymous callers. - boards stay visible to all (physical inventory: address/city/dimension). Frontend (keycloak-js): - Silent SSO (check-sso) — anonymous by default, Войти/Выйти button. - Bearer token attached + refreshed on every API call. - Anonymous UI hides Brand/Manager/Status filters, Brand/Company toggles, appearance menu, collisions and labels; board detail panel (Planfix deep-links) gated to managers. Test manager: user / 1234567890 (realm role `manager`).
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
// Keycloak (OIDC) integration. The app is anonymous by default; logging in as a
|
|
// user with the realm role `manager` unlocks the full data view. Everything here
|
|
// degrades gracefully: if Keycloak is unreachable, initAuth() resolves anyway and
|
|
// the app stays in the anonymous tier.
|
|
import Keycloak from 'keycloak-js';
|
|
|
|
const keycloak = new Keycloak({
|
|
url: 'https://keycloak.intense-sale.ru',
|
|
realm: 'intense-sale',
|
|
clientId: 'mapdash',
|
|
});
|
|
|
|
let manager = false;
|
|
|
|
function computeManager(): boolean {
|
|
const roles = (keycloak.tokenParsed as { realm_access?: { roles?: string[] } } | undefined)
|
|
?.realm_access?.roles ?? [];
|
|
return keycloak.authenticated === true && roles.includes('manager');
|
|
}
|
|
|
|
/**
|
|
* Initialise Keycloak with a silent SSO check (no forced login). Never rejects —
|
|
* on any failure we log and continue as an anonymous visitor.
|
|
*/
|
|
export async function initAuth(): Promise<void> {
|
|
try {
|
|
await keycloak.init({
|
|
onLoad: 'check-sso',
|
|
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
|
|
pkceMethod: 'S256',
|
|
});
|
|
manager = computeManager();
|
|
} catch (e) {
|
|
console.warn('Keycloak init failed — continuing as anonymous', e);
|
|
manager = false;
|
|
}
|
|
}
|
|
|
|
export function isManager(): boolean {
|
|
return manager;
|
|
}
|
|
|
|
export function isAuthenticated(): boolean {
|
|
return keycloak.authenticated === true;
|
|
}
|
|
|
|
export function login(): void {
|
|
// Return to the current view (filters in the URL) after login.
|
|
keycloak.login({ redirectUri: window.location.href });
|
|
}
|
|
|
|
export function logout(): void {
|
|
keycloak.logout({ redirectUri: window.location.origin });
|
|
}
|
|
|
|
/**
|
|
* Authorization header for API calls. Refreshes the token when it is close to
|
|
* expiry. Returns an empty object for anonymous callers (backend then serves the
|
|
* truncated tier).
|
|
*/
|
|
export async function authHeader(): Promise<Record<string, string>> {
|
|
if (!keycloak.authenticated) return {};
|
|
try {
|
|
await keycloak.updateToken(30);
|
|
} catch {
|
|
/* refresh failed — send the current token; backend falls back to anon if invalid */
|
|
}
|
|
return keycloak.token ? { Authorization: `Bearer ${keycloak.token}` } : {};
|
|
}
|