Optimized code and wrote documentation
This commit is contained in:
@@ -1,11 +1,18 @@
|
|||||||
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
|
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
|
||||||
import { AsyncRoute } from "$fresh/src/server/types.ts";
|
import { AsyncRoute } from "$fresh/src/server/types.ts";
|
||||||
|
|
||||||
export interface State {
|
interface AuthenticatedState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: true;
|
||||||
session: CasContent;
|
session: CasContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UnauthenticatedState {
|
||||||
|
isAuthenticated: false;
|
||||||
|
session: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = AuthenticatedState | UnauthenticatedState;
|
||||||
|
|
||||||
export interface AppProperties {
|
export interface AppProperties {
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|||||||
+20
-5
@@ -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 { getCookies } from "$std/http/cookie.ts";
|
||||||
import { getJwtPayload, isJwtValid } from "@popov/jwt";
|
import { getJwtPayload, isJwtValid } from "@popov/jwt";
|
||||||
import { CasContent, LoginJWT, State } from "$root/defaults/interfaces.ts";
|
import { CasContent, LoginJWT, State } from "$root/defaults/interfaces.ts";
|
||||||
@@ -41,11 +41,17 @@ export function getKey(user: string): string {
|
|||||||
return jwtKeyCache[user];
|
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(
|
async function checkAuthentication(
|
||||||
request: Request,
|
request: Request,
|
||||||
context: FreshContext<State>,
|
context: FreshContext<State>,
|
||||||
) {
|
): Promise<Response> {
|
||||||
const cookies = getCookies(request.headers);
|
const cookies = getCookies(request.headers);
|
||||||
if (!cookies["sessionToken"]) {
|
if (!cookies["sessionToken"]) {
|
||||||
context.state.isAuthenticated = false;
|
context.state.isAuthenticated = false;
|
||||||
@@ -59,17 +65,26 @@ export const handler = [
|
|||||||
cookies["sessionToken"],
|
cookies["sessionToken"],
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (context.state.isAuthenticated) {
|
||||||
const session: CasContent =
|
const session: CasContent =
|
||||||
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user;
|
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user;
|
||||||
|
|
||||||
context.state.session = session;
|
context.state.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
return await context.next();
|
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(
|
async function ensureAuthentication(
|
||||||
request: Request,
|
request: Request,
|
||||||
context: FreshContext<State>,
|
context: FreshContext<State>,
|
||||||
) {
|
): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) {
|
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) {
|
||||||
|
|||||||
+22
-5
@@ -1,10 +1,24 @@
|
|||||||
import { FreshContext, Handlers } from "$fresh/server.ts";
|
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";
|
import AppNavigator from "$root/routes/(_islands)/AppNavigator.tsx";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
const apps: Record<string, AppProperties> = {};
|
||||||
async GET(_request, context) {
|
|
||||||
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)")) {
|
for await (const appDir of Deno.readDir("routes/(apps)")) {
|
||||||
if (appDir.isFile) {
|
if (appDir.isFile) {
|
||||||
@@ -26,7 +40,10 @@ export const handler: Handlers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppNavigator apps={context.data} />
|
<AppNavigator apps={context.data} />
|
||||||
|
|||||||
+35
-27
@@ -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 { State } from "$root/defaults/interfaces.ts";
|
||||||
import { parse, type RegularTagNode } from "@melvdouc/xml-parser";
|
import { parse, type RegularTagNode } from "@melvdouc/xml-parser";
|
||||||
import {
|
import {
|
||||||
@@ -13,13 +13,23 @@ import { getKey } from "$root/routes/_middleware.ts";
|
|||||||
|
|
||||||
const CAS = "https://ident.univ-amu.fr/cas";
|
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 [
|
return [
|
||||||
tag.tagName.replace("cas:", ""),
|
tag.tagName.replace("cas:", ""),
|
||||||
tag.children[0].value,
|
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> {
|
function createUserJWT(casResponse: CasResponse): Promise<string> {
|
||||||
const nodes = casResponse.children[1].children.map(getTag);
|
const nodes = casResponse.children[1].children.map(getTag);
|
||||||
const fullUserInfos: Record<string, string | string[]> = {};
|
const fullUserInfos: Record<string, string | string[]> = {};
|
||||||
@@ -28,7 +38,6 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
|
|||||||
if (typeof fullUserInfos[key] == "string") {
|
if (typeof fullUserInfos[key] == "string") {
|
||||||
fullUserInfos[key] = [fullUserInfos[key]];
|
fullUserInfos[key] = [fullUserInfos[key]];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(fullUserInfos[key])) {
|
if (Array.isArray(fullUserInfos[key])) {
|
||||||
fullUserInfos[key].push(value);
|
fullUserInfos[key].push(value);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,12 +46,10 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const oneHour = 60 * 60;
|
|
||||||
|
|
||||||
const payload: LoginJWT = {
|
const payload: LoginJWT = {
|
||||||
iss: "PolyMPR",
|
iss: "PolyMPR",
|
||||||
iat: now,
|
iat: now,
|
||||||
exp: now + oneHour,
|
exp: now + 0xe10,
|
||||||
aud: "PolyMPR",
|
aud: "PolyMPR",
|
||||||
user: fullUserInfos as unknown as CasContent,
|
user: fullUserInfos as unknown as CasContent,
|
||||||
};
|
};
|
||||||
@@ -51,14 +58,32 @@ function createUserJWT(casResponse: CasResponse): Promise<string> {
|
|||||||
return createJwt(payload, key);
|
return createJwt(payload, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// deno-lint-ignore no-explicit-any
|
export const handler: Handlers<null, State> = {
|
||||||
export const handler: Handlers<any, State> = {
|
/**
|
||||||
async GET(request, context) {
|
* 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 url = new URL(request.url);
|
||||||
const ticket = url.searchParams.get("ticket");
|
const ticket = url.searchParams.get("ticket");
|
||||||
const service = `${context.url.origin}/login`;
|
const service = `${context.url.origin}/login`;
|
||||||
|
|
||||||
if (ticket) {
|
if (!ticket) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
Location: context.state.isAuthenticated
|
||||||
|
? "/apps"
|
||||||
|
: `${CAS}/login?service=${service}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${CAS}/serviceValidate?service=${service}&ticket=${ticket}`,
|
`${CAS}/serviceValidate?service=${service}&ticket=${ticket}`,
|
||||||
);
|
);
|
||||||
@@ -86,22 +111,5 @@ export const handler: Handlers<any, State> = {
|
|||||||
status: 302,
|
status: 302,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (context.state.isAuthenticated) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: {
|
|
||||||
Location: "/apps",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: {
|
|
||||||
Location: `${CAS}/login?service=${service}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-4
@@ -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 { State } from "$root/defaults/interfaces.ts";
|
||||||
import { deleteCookie } from "$std/http/cookie.ts";
|
import { deleteCookie } from "$std/http/cookie.ts";
|
||||||
|
|
||||||
const CAS = "https://ident.univ-amu.fr/cas";
|
const CAS = "https://ident.univ-amu.fr/cas";
|
||||||
|
|
||||||
// deno-lint-ignore no-explicit-any
|
export const handler: Handlers<null, State> = {
|
||||||
export const handler: Handlers<any, State> = {
|
/**
|
||||||
GET(_request, context) {
|
* 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) {
|
if (context.state.isAuthenticated) {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user