This repository has been archived on 2025-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
trafficcue-client/src/lib/services/lnv.ts
Cfp babfb526de
Some checks failed
TrafficCue CI / check (push) Failing after 46s
TrafficCue CI / build (push) Failing after 34s
feat: improve OIDC to use refresh tokens
2025-08-19 11:21:27 +02:00

163 lines
4.1 KiB
TypeScript

import { LNV_SERVER } from "./hosts";
import type { OIDCUser } from "./oidc";
export type Capabilities = ("auth" | "reviews" | "ai" | "fuel" | "post")[];
export let capabilities: Capabilities = [];
export let oidcConfig: {
AUTH_URL: string;
CLIENT_ID: string;
TOKEN_URL: string;
} | null = null;
export async function fetchConfig() {
const res = await fetch(LNV_SERVER + "/config");
if (!res.ok) {
throw new Error(`Failed to fetch capabilities: ${res.statusText}`);
}
const data = await res.json();
return data as {
name: string;
version: string;
capabilities: Capabilities;
oidc?: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string };
};
}
export async function getCapabilities() {
if (capabilities.length === 0) {
const config = await fetchConfig();
capabilities = config.capabilities;
}
return capabilities;
}
export async function getOIDCConfig() {
if (oidcConfig) {
return oidcConfig;
}
const config = await fetchConfig();
if (config.oidc) {
oidcConfig = {
AUTH_URL: config.oidc.AUTH_URL,
CLIENT_ID: config.oidc.CLIENT_ID,
TOKEN_URL: config.oidc.TOKEN_URL,
};
}
return oidcConfig;
}
export async function hasCapability(
capability: Capabilities[number],
): Promise<boolean> {
const caps = await getCapabilities();
return caps.includes(capability);
}
export async function refreshToken() {
const config = await getOIDCConfig();
if(!config) throw new Error("Server does not support OIDC.");
const refresh_token = localStorage.getItem("lnv-refresh");
if(!refresh_token) throw new Error("No refresh token.")
const params = new URLSearchParams();
params.append("grant_type", "refresh_token");
params.append("refresh_token", refresh_token);
params.append("client_id", config.CLIENT_ID);
const res = await fetch(config.TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params
});
const data = (await res.json()) as OIDCUser;
if(!res.ok) {
console.error("Refreshing token: " + res.status + " " + res.statusText)
console.error(data);
}
console.log(data);
localStorage.setItem("lnv-id", data.id_token);
localStorage.setItem("lnv-token", data.access_token);
localStorage.setItem("lnv-refresh", data.refresh_token);
}
export async function authFetch(url: string, params: RequestInit): ReturnType<typeof fetch> {
let res = await fetch(url, params);
if(res.status == 401) {
await refreshToken();
}
res = await fetch(url, params);
if(res.status == 401) {
console.error("Server is misconfigured.");
}
return res;
}
export interface Review {
user_id: string;
username: string;
rating: number;
comment: string;
}
export async function getReviews(location: WorldLocation) {
if (!(await hasCapability("reviews"))) {
throw new Error("Reviews capability is not available");
}
const res = await fetch(
LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`,
);
if (!res.ok) {
throw new Error(`Failed to fetch reviews: ${res.statusText}`);
}
const data = await res.json();
return data as Review[];
}
export async function postReview(
location: WorldLocation,
review: Omit<Review, "user_id" | "username">,
) {
if (!(await hasCapability("reviews"))) {
throw new Error("Reviews capability is not available");
}
const token = localStorage.getItem("lnv-token");
if (!token) {
throw new Error("User is not authenticated");
}
const res = await fetch(LNV_SERVER + `/review`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...review,
lat: location.lat,
lon: location.lon,
}),
});
if (!res.ok) {
throw new Error(`Failed to post review: ${res.statusText}`);
}
return await res.json();
}
export async function ai(query: string, location?: WorldLocation) {
if (!(await hasCapability("ai"))) {
throw new Error("AI capability is not available");
}
const res = await fetch(LNV_SERVER + `/ai`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text: query,
coords: location,
}),
});
if (!res.ok) {
throw new Error(`Failed to get AI response: ${res.statusText}`);
}
return await res.text();
}