Files
alva/src/felsokning/plattform.ts
T
Claude 9dbd0aa5f7 Live Share-behörighetsnivåer: kund/partner/intern med återkallelse
Modulspecifikationens sista del implementerad:

- Ny tabell delningar: återkallbara länkar med behörighetsnivå
  (kund/partner/intern), organisationsknutna via ärendet.
- Nivåstyrd filtrering på serversidan i GET /api/delad/{kod}:
  kund utesluter kategoribyten, hypoteser och AI-dialog; partner
  utesluter kategoribyten och AI-dialog (hypoteser visas, märkta ej
  verifierade); intern visar allt. Ärendets ursprungliga delningskod
  fungerar bakåtkompatibelt som kundnivå. Svaret bär nivån.
- Nya endpoints: skapa/lista delningar per ärende och återkalla per
  kod — alltid organisationskontrollerat; en återkallad länk ger 404.
- Klienten: delningshanterare i rapportfliken (självhostat läge) med
  skapa per nivå, kopiera och återkalla — varje åtgärd loggas i
  ärendet; publika delningssidan visar nivåanpassad notis och renderar
  serverfiltrerade händelser utan att dölja partner-/interninnehåll.
- OpenAPI-specen utökad (Delning, DelningsNiva, tre nya operationer)
  och maskinvaliderad.

Verifierat: integrationstestets 25 kontroller gröna mot Postgres 16 —
inkl. att kundkoden filtrerar hypoteser, partnernivån visar dem men
döljer kategoribyten, internnivån visar allt, org B inte kan skapa
delning av org A:s ärende och att återkallad länk ger 404. 29
vitest-tester gröna, produktionsbygge ok, Playwright-röktest grönt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 07:52:29 +00:00

169 lines
5.3 KiB
TypeScript

// Självhostat läge: klienten pratar med plattformstjänsten i klustret
// (services/plattform) i stället för Supabase. Aktiveras vid bygget med
// VITE_PLATTFORM_URL; utan den används Supabase-läget precis som förut.
export const PLATTFORM_URL = (import.meta.env.VITE_PLATTFORM_URL as string | undefined)?.replace(/\/$/, "");
const TOKEN_NYCKEL = "gf-plattform-token";
const KONTO_NYCKEL = "gf-plattform-konto";
export type PlattformRoll = "tekniker" | "arbetsledare" | "admin";
export interface PlattformKonto {
namn: string;
roll: PlattformRoll;
organisation: string;
}
export function plattformAktiv(): boolean {
return !!PLATTFORM_URL;
}
export function plattformToken(): string | null {
return localStorage.getItem(TOKEN_NYCKEL);
}
export function plattformKonto(): PlattformKonto | null {
const rad = localStorage.getItem(KONTO_NYCKEL);
if (!rad || !plattformToken()) return null;
try {
return JSON.parse(rad) as PlattformKonto;
} catch {
return null;
}
}
export function loggaUtPlattform(): void {
localStorage.removeItem(TOKEN_NYCKEL);
localStorage.removeItem(KONTO_NYCKEL);
}
async function authAnrop(vag: string, kropp: object): Promise<PlattformKonto> {
const res = await fetch(`${PLATTFORM_URL}${vag}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(kropp),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((data as { error?: string }).error ?? `Fel ${res.status}`);
const { token, namn, roll, organisation } = data as PlattformKonto & { token: string };
localStorage.setItem(TOKEN_NYCKEL, token);
const konto: PlattformKonto = { namn, roll, organisation };
localStorage.setItem(KONTO_NYCKEL, JSON.stringify(konto));
return konto;
}
export function loggaInPlattform(epost: string, losenord: string): Promise<PlattformKonto> {
return authAnrop("/api/auth/logga-in", { epost, losenord });
}
// Registrering skapar en ny organisation med användaren som
// systemadministratör; övriga användare skapas av admin.
export function registreraPlattform(
epost: string,
losenord: string,
namn: string,
organisation: string,
): Promise<PlattformKonto> {
return authAnrop("/api/auth/registrera", { epost, losenord, namn, organisation });
}
// Användarhantering (kräver admin-roll; servern verifierar).
export interface PlattformAnvandare {
id: string;
epost: string;
namn: string;
roll: PlattformRoll;
}
export async function hamtaAnvandare(): Promise<PlattformAnvandare[]> {
const res = await plattformFetch("/api/anvandare");
if (!res.ok) throw new Error(`Fel ${res.status}`);
return ((await res.json()) as { anvandare: PlattformAnvandare[] }).anvandare;
}
export async function skapaAnvandare(
epost: string,
losenord: string,
namn: string,
roll: PlattformRoll,
): Promise<void> {
const res = await plattformFetch("/api/anvandare", {
method: "POST",
body: JSON.stringify({ epost, losenord, namn, roll }),
});
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(data.error ?? `Fel ${res.status}`);
}
}
// Live Share-delningar: återkallbara länkar med behörighetsnivå.
export type DelningsNiva = "kund" | "partner" | "intern";
export interface Delning {
kod: string;
niva: DelningsNiva;
skapad: string;
aterkallad: string | null;
}
export async function hamtaDelningar(arendeId: string): Promise<Delning[]> {
const res = await plattformFetch(`/api/arenden/${arendeId}/delningar`);
if (!res.ok) throw new Error(`Fel ${res.status}`);
return ((await res.json()) as { delningar: Delning[] }).delningar;
}
export async function skapaDelning(arendeId: string, niva: DelningsNiva): Promise<string> {
const res = await plattformFetch(`/api/arenden/${arendeId}/delningar`, {
method: "POST",
body: JSON.stringify({ niva }),
});
if (!res.ok) throw new Error(`Fel ${res.status}`);
return ((await res.json()) as { kod: string }).kod;
}
export async function aterkallaDelning(kod: string): Promise<void> {
const res = await plattformFetch(`/api/delningar/${kod}/aterkalla`, { method: "POST" });
if (!res.ok) throw new Error(`Fel ${res.status}`);
}
// Organisationsöversikt (arbetsledare/admin; servern verifierar).
export interface OversiktsRad {
id: string;
nummer: number;
skapad: string;
delningskod: string | null;
metodik_id: string | null;
antal_handelser: number;
forsta: string | null;
senaste: string | null;
avslutat: boolean;
objekt: string | null;
felbeskrivning: string | null;
tekniker: string[] | null;
}
export async function hamtaOversikt(): Promise<OversiktsRad[]> {
const res = await plattformFetch("/api/oversikt");
if (!res.ok) throw new Error(`Fel ${res.status}`);
return ((await res.json()) as { arenden: OversiktsRad[] }).arenden;
}
// Autentiserat anrop mot plattformen. En utgången token rensas (401) så
// att appen faller tillbaka till lokalt läge tills nästa inloggning.
export async function plattformFetch(vag: string, init?: RequestInit): Promise<Response> {
const token = plattformToken();
if (!token) throw new Error("Ej inloggad");
const res = await fetch(`${PLATTFORM_URL}${vag}`, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
Authorization: `Bearer ${token}`,
},
});
if (res.status === 401) loggaUtPlattform();
return res;
}