314 lines
9.1 KiB
TypeScript
314 lines
9.1 KiB
TypeScript
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<string, string> = {
|
|
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<string, string> = {
|
|
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<string | null> = 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");
|
|
}
|
|
} |