import type { Activity, ActivityTemplate, ActivityType, Sykkelaksjon } from './message'; import type { SykkelaksjonOpenid } from './openid'; import './style.css' import Alpine from 'alpinejs' import * as openidClient from 'openid-client'; // ---------------- HTML ELEMENT SETUP ---------------- // Set the default date to today let datepicker = document.getElementById("activity-date") as HTMLInputElement; datepicker.valueAsDate = new Date(); // Override what the activity form does let activityForm = document.getElementById("activity-form") as HTMLFormElement; activityForm.onsubmit = () => false; // Override what the activity button does document.getElementById("submit-activity-button")?.addEventListener("click", () => submitFormToUrl(apiUrl + "/submitActivity")); document.getElementById("submit-activity-template-button")?.addEventListener("click", () => submitFormToUrl(apiUrl + "/submitActivityTemplate")); let loadingDiv = document.getElementById("loading-div"); let contentDiv = document.getElementById("content-div"); // ---------------- ALPINE STATE ----------------------- interface AlpineState { data: Sykkelaksjon, registerTemplateActivity: (template: ActivityTemplate) => void, deleteTemplateActivity: (template: ActivityTemplate) => void, deleteActivity: (activity: Activity) => void, deleteActivityType: (activityType: ActivityType) => void, makeAdministrator: (userId: number) => void, removeAdministrator: (userId: number) => void, deleteUser: (userId: number) => void, onClickHideCheckbox: (event: PointerEvent) => void, init: () => void, setServerMessage: (newMessage: Sykkelaksjon) => void, getServerMessage: () => Sykkelaksjon, }; let defaultData: Sykkelaksjon = { name: "ukjent bruker", isAdmin: false, unitConversionNecessary: false, activityTypes: [], activityTemplates: [], activities: [], otherUsers: [] } let alpineState: AlpineState = { data: defaultData, registerTemplateActivity: registerTemplateActivity, deleteTemplateActivity: deleteTemplateActivity, deleteActivity: deleteActivity, deleteActivityType: deleteActivityType, makeAdministrator: makeAdministrator, removeAdministrator: removeAdministrator, deleteUser: deleteUser, onClickHideCheckbox: onClickHideCheckbox, init() { this.data = defaultData; }, setServerMessage(newMessage: Sykkelaksjon) { this.data = newMessage; }, getServerMessage(): Sykkelaksjon { return this.data; } } window.Alpine = Alpine Alpine.store('state', alpineState); Alpine.start(); // ------------------- FUNCTIONS TO BE CALLED FROM THE HTML ----------------------- function registerTemplateActivity(template: ActivityTemplate): void { let form = new FormData(); form.set("activity-type", template.activityType.id?.toString() ?? ""); form.set("activity-description", template.name); form.set("activity-distance", template.numberOfUnits.toString()); var currentDate = new Date(); form.set("activity-date", `${currentDate.getFullYear().toString().padStart(4, '0')}-${(currentDate.getMonth()+1).toString().padStart(2, '0')}-${currentDate.getDate().toString().padStart(2, '0')}`); submitFormToUrl(apiUrl + "/submitActivity", form); } function deleteTemplateActivity(template: ActivityTemplate): void { let form = new FormData(); form.set("activity-template-id", template.id?.toString() ?? ""); submitFormToUrl(apiUrl + "/deleteActivityTemplate", form, "DELETE"); } function deleteActivity(activity: Activity): void { let form = new FormData(); form.set("activity-id", activity.id?.toString() ?? ""); submitFormToUrl(apiUrl + "/deleteActivity", form, "DELETE"); } function postActivityType(): void { let activityTypeForm = document.getElementById("activity-type-form") as HTMLFormElement; let activtyTypeData = new FormData(activityTypeForm); submitFormToUrl(apiUrl + "/addActivityType", activtyTypeData); } declare global { interface Window { postActivityType: () => void } } window.postActivityType = postActivityType; function deleteActivityType(activityType: ActivityType) { let form = new FormData(); form.set("activity-type-id", activityType.id?.toString() ?? ""); submitFormToUrl(apiUrl + "/deleteActivityType", form, "DELETE"); } function makeAdministrator(userId: number) { let form = new FormData(); form.set("user-id", userId.toString()); submitFormToUrl(apiUrl + "/makeAdmin", form, "PUT") } function removeAdministrator(userId: number) { let form = new FormData(); form.set("user-id", userId.toString()); submitFormToUrl(apiUrl + "/removeAdmin", form, "PUT") } function deleteUser(userId: number) { let form = new FormData(); form.set("user-id", userId.toString()); submitFormToUrl(apiUrl + "/deleteUser", form, "DELETE"); } // ------------------- DECIDE WHERE TO CONNECT ------------- let apiUrl: string; if (import.meta.env.DEV) { apiUrl = "http://localhost:8080/api"; } else { apiUrl = document.location.protocol + "//" + document.location.host + "/api"; } // ------------------- OPENID CONNECT STUFF ---------------- let discoveryUri = localStorage.getItem("sykkelaksjon-discovery-uri"); let clientId = localStorage.getItem("sykkelaksjon-client-id"); if (!discoveryUri || !clientId) { await fetch(apiUrl + "/openid") .then(response => response.json()) .then(json => { let received = json as SykkelaksjonOpenid; discoveryUri = received.openid_discovery_uri; clientId = received.client_id; localStorage.setItem("sykkelaksjon-discovery-uri", discoveryUri); localStorage.setItem("sykkelaksjon-client-id", clientId); }) } let refreshToken: string | null = null; let config: openidClient.Configuration | null = null; if (discoveryUri && clientId) { let server: URL = new URL(discoveryUri); config = await openidClient.discovery(server, clientId); let getCurrentUrl: ((...args: any) => URL) = () => { return new URL(window.location.href); } let currentUrl = getCurrentUrl(); let codeVerifier = localStorage.getItem("sykkelaksjon-codeverifier"); if (!currentUrl.searchParams.has("session_state") || !currentUrl.searchParams.has("iss") || !currentUrl.searchParams.has("code") || !codeVerifier) { let redirectUri = window.location.href; let scope = "openid profile"; codeVerifier = openidClient.randomPKCECodeVerifier(); localStorage.setItem("sykkelaksjon-codeverifier", codeVerifier); let codeChallenge = await openidClient.calculatePKCECodeChallenge(codeVerifier); let state: string; let parameters: Record = { redirect_uri: redirectUri, scope, code_challenge: codeChallenge, code_challenge_method: 'S256' }; if (!config.serverMetadata().supportsPKCE()) { state = openidClient.randomState(); parameters.state = state; localStorage.setItem("sykkelaksjon-state", state); } let redirectTo: URL = openidClient.buildAuthorizationUrl(config, parameters); window.location.href = redirectTo.toString(); } else { window.history.pushState({}, "", window.location.protocol + "//" + window.location.host); let state = localStorage.getItem("sykkelaksjon-state"); let authorizationData: Record = { pkceCodeVerifier: codeVerifier }; if (state) { authorizationData['state'] = state; } let tokens = await openidClient.authorizationCodeGrant( config, currentUrl, authorizationData ); refreshToken = tokens.refresh_token ?? null; } } // ------------------- REQUESTING DATA FROM THE API ------------------------------- let receivedData = false; const getAccessToken: () => Promise = async () => { if (!refreshToken || !config) { return null; } let receivedTokens = await openidClient.refreshTokenGrant(config, refreshToken); refreshToken = receivedTokens.refresh_token ?? null; return receivedTokens.access_token; } const submitFormToUrl = (url: string, formData?: FormData, method?: string) => { if (!refreshToken) { return; } if (!formData) { formData = new FormData(activityForm); } if (!method) { method = 'POST'; } getAccessToken().then(token => { if (token) { fetch( url, { method: method, body: formData, credentials: 'include', headers: { "Authorization": `Bearer ${token}` } } ).then(() => refreshData(token)); } }); } const refreshData = async (accessToken?: string) => { if (!refreshToken) { return; } if (!accessToken) { let possibleAccessToken = await getAccessToken(); if (!possibleAccessToken) { return; } accessToken = possibleAccessToken; } await fetch(apiUrl, {headers: {"Authorization": `Bearer ${accessToken}`}}) .then(response => response.json()) .then(message => { (Alpine.store('state') as AlpineState).setServerMessage(message) receivedData = true; }); } // Finally, refresh data await refreshData(); // After data is refreshed, display it if (receivedData) { loadingDiv?.setAttribute("style", "display: none"); contentDiv?.setAttribute("style", "display: block"); } function onClickHideCheckbox(event: PointerEvent) { let possibleCheckbox = event.target; if (possibleCheckbox instanceof HTMLInputElement) { let formData = new FormData(); let url; let isChecked = possibleCheckbox.checked; if (isChecked) { url = apiUrl + "/hideMe" } else { url = apiUrl + "/showMe" } submitFormToUrl(url, formData, "PUT"); } }