Optimized code and wrote documentation

This commit is contained in:
Kevin FEDYNA
2025-01-22 11:15:43 +01:00
parent 3ce1273455
commit 8a5461827e
5 changed files with 113 additions and 61 deletions
+9 -2
View File
@@ -1,11 +1,18 @@
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
import { AsyncRoute } from "$fresh/src/server/types.ts";
export interface State {
isAuthenticated: boolean;
interface AuthenticatedState {
isAuthenticated: true;
session: CasContent;
}
interface UnauthenticatedState {
isAuthenticated: false;
session: undefined;
}
export type State = AuthenticatedState | UnauthenticatedState;
export interface AppProperties {
name: string;
icon: string;
+22 -7
View File
@@ -1,4 +1,4 @@
import { FreshContext } from "$fresh/server.ts";
import { FreshContext, MiddlewareHandler } from "$fresh/server.ts";
import { getCookies } from "$std/http/cookie.ts";
import { getJwtPayload, isJwtValid } from "@popov/jwt";
import { CasContent, LoginJWT, State } from "$root/defaults/interfaces.ts";
@@ -41,11 +41,17 @@ export function getKey(user: string): string {
return jwtKeyCache[user];
}
export const handler = [
export const handler: MiddlewareHandler<State>[] = [
/**
* Check if user is authenticated and add session to context accordingly.
* @param request The HTTP incomming request.
* @param context The Fresh context object with custom `State`.
* @returns The response from the next middleware.
*/
async function checkAuthentication(
request: Request,
context: FreshContext<State>,
) {
): Promise<Response> {
const cookies = getCookies(request.headers);
if (!cookies["sessionToken"]) {
context.state.isAuthenticated = false;
@@ -59,17 +65,26 @@ export const handler = [
cookies["sessionToken"],
key,
);
const session: CasContent =
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user;
context.state.session = session;
if (context.state.isAuthenticated) {
const session: CasContent =
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user;
context.state.session = session;
}
return await context.next();
},
/**
* Check if page can be accessed with or without authentication.
* Redirect if the page is private and the user isn't authenticated.
* @param request The HTTP incomming request.
* @param context The Fresh context object with `State` set up.
* @returns The response from the next middleware or from the page.
*/
async function ensureAuthentication(
request: Request,
context: FreshContext<State>,
) {
): Promise<Response> {
const url = new URL(request.url);
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) {
+22 -5
View File
@@ -1,10 +1,24 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { AppProperties } from "$root/defaults/interfaces.ts";
import { AppProperties, State } from "$root/defaults/interfaces.ts";
import AppNavigator from "$root/routes/(_islands)/AppNavigator.tsx";
export const handler: Handlers = {
async GET(_request, context) {
const apps: Record<string, AppProperties> = {};
const apps: Record<string, AppProperties> = {};
export const handler: Handlers<Record<string, AppProperties>, State> = {
/**
* Generate the app catalog page from pages.
* Catalog is only computed once, then the cached version is used.
* @param _request The HTTP incomming request.
* @param context The Fresh context with `State`.
* @returns The rendered page with all apps as catalog.
*/
async GET(
_request: Request,
context: FreshContext<State, Record<string, AppProperties>>,
): Promise<Response> {
if (Object.keys(apps).length != 0) {
return context.render(apps);
}
for await (const appDir of Deno.readDir("routes/(apps)")) {
if (appDir.isFile) {
@@ -26,7 +40,10 @@ export const handler: Handlers = {
};
// deno-lint-ignore require-await
export default async function Apps(_request: Request, context: FreshContext) {
export default async function Apps(
_request: Request,
context: FreshContext<State, Record<string, AppProperties>>,
) {
return (
<>
<AppNavigator apps={context.data} />
+50 -42
View File
@@ -1,4 +1,4 @@
import { Handlers } from "$fresh/server.ts";
import { FreshContext, Handlers } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
import { parse, type RegularTagNode } from "@melvdouc/xml-parser";
import {
@@ -13,13 +13,23 @@ import { getKey } from "$root/routes/_middleware.ts";
const CAS = "https://ident.univ-amu.fr/cas";
function getTag(tag: CasTagNode): [string, string] {
/**
* Get the tag node value without "cas:" prefix in name.
* @param tag The CAS tag node.
* @returns The `[name, value]` pair.
*/
function getTag(tag: CasTagNode): [name: string, value: string] {
return [
tag.tagName.replace("cas:", ""),
tag.children[0].value,
];
}
/**
* Gets the user JWT token with a validity period of one hour.
* @param casResponse The CAS reponse parsed from XML.
* @returns The user JWT session token.
*/
function createUserJWT(casResponse: CasResponse): Promise<string> {
const nodes = casResponse.children[1].children.map(getTag);
const fullUserInfos: Record<string, string | string[]> = {};
@@ -28,7 +38,6 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
if (typeof fullUserInfos[key] == "string") {
fullUserInfos[key] = [fullUserInfos[key]];
}
if (Array.isArray(fullUserInfos[key])) {
fullUserInfos[key].push(value);
} else {
@@ -37,12 +46,10 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
});
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
const payload: LoginJWT = {
iss: "PolyMPR",
iat: now,
exp: now + oneHour,
exp: now + 0xe10,
aud: "PolyMPR",
user: fullUserInfos as unknown as CasContent,
};
@@ -51,51 +58,39 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
return createJwt(payload, key);
}
// deno-lint-ignore no-explicit-any
export const handler: Handlers<any, State> = {
async GET(request, context) {
export const handler: Handlers<null, State> = {
/**
* Handles all CAS protocol requests.
* @param request The incomming HTTP request.
* @param context The Fresh context with `State`.
* @returns The redirect corresponding to each step of the CAS protocol.
*/
async GET(
request: Request,
context: FreshContext<State, null>,
): Promise<Response> {
const url = new URL(request.url);
const ticket = url.searchParams.get("ticket");
const service = `${context.url.origin}/login`;
if (ticket) {
const response = await fetch(
`${CAS}/serviceValidate?service=${service}&ticket=${ticket}`,
);
const body = parse(await response.text()) as [RegularTagNode];
const casResponse = body[0].children[0] as CasResponse;
if (casResponse.tagName != "cas:authenticationSuccess") {
return new Response(null, {
status: 302,
headers: {
Location: `${CAS}/login?service=${service}`,
},
});
}
const headers = new Headers();
setCookie(headers, {
name: "sessionToken",
value: await createUserJWT(casResponse),
});
headers.set("Location", "/apps");
return new Response(null, {
status: 302,
headers,
});
}
if (context.state.isAuthenticated) {
if (!ticket) {
return new Response(null, {
status: 302,
headers: {
Location: "/apps",
Location: context.state.isAuthenticated
? "/apps"
: `${CAS}/login?service=${service}`,
},
});
} else {
}
const response = await fetch(
`${CAS}/serviceValidate?service=${service}&ticket=${ticket}`,
);
const body = parse(await response.text()) as [RegularTagNode];
const casResponse = body[0].children[0] as CasResponse;
if (casResponse.tagName != "cas:authenticationSuccess") {
return new Response(null, {
status: 302,
headers: {
@@ -103,5 +98,18 @@ export const handler: Handlers<any, State> = {
},
});
}
const headers = new Headers();
setCookie(headers, {
name: "sessionToken",
value: await createUserJWT(casResponse),
});
headers.set("Location", "/apps");
return new Response(null, {
status: 302,
headers,
});
},
};
+9 -4
View File
@@ -1,12 +1,17 @@
import { Handlers } from "$fresh/server.ts";
import { FreshContext, Handlers } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
import { deleteCookie } from "$std/http/cookie.ts";
const CAS = "https://ident.univ-amu.fr/cas";
// deno-lint-ignore no-explicit-any
export const handler: Handlers<any, State> = {
GET(_request, context) {
export const handler: Handlers<null, State> = {
/**
* Logout of amU CAS SSO system.
* @param _request The HTTP incomming request.
* @param context The Fresh context with `State`.
* @returns A redirect response to either CAS logout or home.
*/
GET(_request: Request, context: FreshContext<State, null>): Response {
if (context.state.isAuthenticated) {
const headers = new Headers();