diff --git a/app/src/api/schema.ts b/app/src/api/schema.ts index c9b7069..4f4b938 100644 --- a/app/src/api/schema.ts +++ b/app/src/api/schema.ts @@ -89,6 +89,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/locks/stream": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Watch Locks */ + get: operations["watch_locks_api_locks_stream_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/locks/{lock_id}": { parameters: { query?: never; @@ -135,7 +152,7 @@ export interface components { * @description Statically known HTTP problem types using the [type URI scheme](https://datatracker.ietf.org/doc/rfc4151/) * @enum {string} */ - HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND"; + HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026,FORBIDDEN_TO_OPERATE" | "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND"; /** Lock */ Lock: { /** Name */ @@ -178,14 +195,17 @@ export interface components { }; /** UserStatus */ UserStatus: { - /** Is Logged In */ - is_logged_in: boolean; /** Is Authorized */ is_authorized: boolean; - /** Guaranteed Session Until */ - guaranteed_session_until: string | null; + /** + * Guaranteed Session Until + * Format: date-time + */ + guaranteed_session_until: string; /** Username */ - username: string | null; + username: string; + /** Ccchh Roles */ + ccchh_roles: string[]; }; /** ValidationError */ ValidationError: { @@ -332,6 +352,35 @@ export interface operations { }; }; }; + watch_locks_api_locks_stream_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": unknown; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": components["schemas"]["HttpProblemDetail"]; + }; + }; + }; + }; operate_lock_api_locks__lock_id__patch: { parameters: { query?: never; @@ -365,6 +414,15 @@ export interface operations { "application/json": components["schemas"]["HttpProblemDetail"]; }; }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HttpProblemDetail"]; + }; + }; /** @description Not Found */ 404: { headers: { diff --git a/app/src/assets/main.ts b/app/src/assets/main.ts index 4f61428..205f64f 100644 --- a/app/src/assets/main.ts +++ b/app/src/assets/main.ts @@ -1,5 +1,5 @@ import {Fetcher} from "openapi-typescript-fetch" -import type {paths} from "../api/schema" +import type {components, paths} from "../api/schema" import type {ui} from "../i18n/ui.ts" const fetcher = Fetcher.for() @@ -29,7 +29,7 @@ declare global { lang: keyof typeof ui; doors: Array; auth: AuthType; - doorAction: (action: 'unlock' | 'lock', doorId: string) => void; + doorAction: (action: "unlock" | "lock", doorId: string) => void; } } @@ -105,18 +105,18 @@ async function checkUser() { if (e instanceof getUserInfo.Error) { const error = e.getActualType() - if (error.status === 401) { - if (!auth.recentLogout) - auth.recentLogout = auth.authenticated // set recentLogout true, if user was logged in before - auth.authenticated = false - auth.authorized = false - auth.until = null - auth.username = "" - } else if (error.status >= 500 && error.status < 600) { + + if (error.status >= 500 && error.status < 600) { apiError.current = "serverError" - } else { - console.error("unknown error:", error) } + + if (!auth.recentLogout) + auth.recentLogout = auth.authenticated // set recentLogout true, if user was logged in before + auth.authenticated = false + auth.authorized = false + auth.until = null + auth.username = "" + } } finally { localStorage.setItem("auth", JSON.stringify(auth)) @@ -125,15 +125,24 @@ async function checkUser() { } } -async function fetchDoors() { +async function subscribeDoorEvents() { if (doors.length === 0) { loading.doors = true } refresh() - const getDoors = fetcher.path("/api/locks/").method("get").create() - try { - const {data: doorInfo} = await getDoors({}) + const evtSource = new EventSource("/api/locks/stream") + + evtSource.onerror = () => { + if (!window.navigator.onLine) { + apiError.current = "networkError" + } else { + apiError.current = "serverError" + } + } + + evtSource.onmessage = (event) => { + const doorInfo: Array = JSON.parse(event.data) apiError.current = null while (doors.length) { @@ -164,29 +173,6 @@ async function fetchDoors() { loading.doors = false refresh() - } catch (e) { - // check which operation threw the exception - if (e instanceof getDoors.Error) { - const error = e.getActualType() - - if (error.status === 401) { - console.log("unauthorized") - loading.doors = false - refresh() - } else if (error.status >= 500 && error.status < 600) { - apiError.current = "serverError" - clearInterval(doorsInterval) - } else { - console.error("unknown error:", error) - } - } - - if (e instanceof Error) { - switch (e.name) { - case "TypeError": - apiError.current = "networkError" - } - } } } @@ -295,7 +281,7 @@ function refresh() { loadAuthFromLocalStorage() -const doorsInterval = setInterval(fetchDoors, 250) // TODO: replace with SSE +subscribeDoorEvents() checkUser() document.addEventListener("loadeddata", () => {