From 85766ffeed8e29f864ca771470033e777456c262 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Tue, 21 Jan 2025 22:31:36 +0100
Subject: [PATCH 1/6] changed login and logout to adapt to domain and added ToU
---
fresh.gen.ts | 3 -
routes/(apps)/notes/(_props)/props.ts | 1 -
.../(apps)/notes/partials/(admin)/courses.tsx | 27 ++-
.../notes/partials/(admin)/students.tsx | 17 --
routes/(apps)/notes/partials/index.tsx | 9 +-
routes/(apps)/notes/partials/notes.tsx | 27 ++-
routes/about.tsx | 164 +++++++++++++++++-
routes/index.tsx | 3 +-
routes/login.tsx | 8 +-
routes/logout.tsx | 3 +-
10 files changed, 199 insertions(+), 63 deletions(-)
delete mode 100644 routes/(apps)/notes/partials/(admin)/students.tsx
diff --git a/fresh.gen.ts b/fresh.gen.ts
index a256829..9d9666d 100644
--- a/fresh.gen.ts
+++ b/fresh.gen.ts
@@ -11,7 +11,6 @@ import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/par
import * as $_apps_mobility_partials_students from "./routes/(apps)/mobility/partials/students.tsx";
import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx";
import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx";
-import * as $_apps_notes_partials_admin_students from "./routes/(apps)/notes/partials/(admin)/students.tsx";
import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
import * as $_apps_students_api_example from "./routes/(apps)/students/api/example.ts";
@@ -52,8 +51,6 @@ const manifest = {
"./routes/(apps)/notes/index.tsx": $_apps_notes_index,
"./routes/(apps)/notes/partials/(admin)/courses.tsx":
$_apps_notes_partials_admin_courses,
- "./routes/(apps)/notes/partials/(admin)/students.tsx":
- $_apps_notes_partials_admin_students,
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
"./routes/(apps)/students/api/example.ts": $_apps_students_api_example,
diff --git a/routes/(apps)/notes/(_props)/props.ts b/routes/(apps)/notes/(_props)/props.ts
index c3d9f0f..36b0f28 100644
--- a/routes/(apps)/notes/(_props)/props.ts
+++ b/routes/(apps)/notes/(_props)/props.ts
@@ -7,7 +7,6 @@ const properties: AppProperties = {
index: "Homepage",
notes: "Notes",
courses: "Courses management",
- students: "Students management",
},
adminOnly: ["courses", "students"],
hint: "Student grading management",
diff --git a/routes/(apps)/notes/partials/(admin)/courses.tsx b/routes/(apps)/notes/partials/(admin)/courses.tsx
index 9d361cf..3ac215d 100644
--- a/routes/(apps)/notes/partials/(admin)/courses.tsx
+++ b/routes/(apps)/notes/partials/(admin)/courses.tsx
@@ -1,17 +1,14 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
+import {
+ getPartialsConfig,
+ makePartials,
+} from "$root/defaults/makePartials.tsx";
+import { FreshContext } from "$fresh/server.ts";
+import { State } from "$root/routes/_middleware.ts";
-type ModulesProps = Record;
-
-export const config: RouteConfig = {
- skipAppWrapper: true,
- skipInheritedLayouts: true,
-};
-
-export default function Modules(_props: ModulesProps) {
- return (
-
- notes
-
- );
+// deno-lint-ignore require-await
+async function Courses(_request: Request, context: FreshContext) {
+ return Welcome to {context.state.session?.displayName}.
;
}
+
+export const config = getPartialsConfig();
+export default makePartials(Courses);
diff --git a/routes/(apps)/notes/partials/(admin)/students.tsx b/routes/(apps)/notes/partials/(admin)/students.tsx
deleted file mode 100644
index 9d361cf..0000000
--- a/routes/(apps)/notes/partials/(admin)/students.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
-
-type ModulesProps = Record;
-
-export const config: RouteConfig = {
- skipAppWrapper: true,
- skipInheritedLayouts: true,
-};
-
-export default function Modules(_props: ModulesProps) {
- return (
-
- notes
-
- );
-}
diff --git a/routes/(apps)/notes/partials/index.tsx b/routes/(apps)/notes/partials/index.tsx
index 85fcd2b..2971e0e 100644
--- a/routes/(apps)/notes/partials/index.tsx
+++ b/routes/(apps)/notes/partials/index.tsx
@@ -2,11 +2,12 @@ import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
+import { FreshContext } from "$fresh/server.ts";
+import { State } from "$root/routes/_middleware.ts";
-type NotesIndexProps = Record;
-
-export function Index(_props: NotesIndexProps) {
- return bip boup;
+// deno-lint-ignore require-await
+export async function Index(_request: Request, context: FreshContext) {
+ return Welcome to {context.state.session?.displayName}.
;
}
export const config = getPartialsConfig();
diff --git a/routes/(apps)/notes/partials/notes.tsx b/routes/(apps)/notes/partials/notes.tsx
index 9d361cf..1e3bbc1 100644
--- a/routes/(apps)/notes/partials/notes.tsx
+++ b/routes/(apps)/notes/partials/notes.tsx
@@ -1,17 +1,14 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
+import {
+ getPartialsConfig,
+ makePartials,
+} from "$root/defaults/makePartials.tsx";
+import { FreshContext } from "$fresh/server.ts";
+import { State } from "$root/routes/_middleware.ts";
-type ModulesProps = Record;
-
-export const config: RouteConfig = {
- skipAppWrapper: true,
- skipInheritedLayouts: true,
-};
-
-export default function Modules(_props: ModulesProps) {
- return (
-
- notes
-
- );
+// deno-lint-ignore require-await
+async function Notes(_request: Request, context: FreshContext) {
+ return Welcome to {context.state.session?.displayName}.
;
}
+
+export const config = getPartialsConfig();
+export default makePartials(Notes);
diff --git a/routes/about.tsx b/routes/about.tsx
index 05fb902..17415ee 100644
--- a/routes/about.tsx
+++ b/routes/about.tsx
@@ -9,7 +9,169 @@ export default async function About(_request: Request, _context: FreshContext) {
PolyMPR is born from the will to enhance Polytech INFO department's HR
infrastructure.
- Terms of Use
+ Terms of Use
+
+
+ Last updated: 21th Jan. 2025
+
+
+
+ By accessing and using this website through the Aix-Marseille University
+ (AMU) Single Sign-On (SSO) authentication system, you agree to comply
+ with these Terms and Conditions. Please read them carefully before
+ proceeding.
+
+ 1. Acceptance of Terms
+ By logging in with your AMU SSO credentials, you confirm that:
+
+ -
+ You are an authorized user of Aix-Marseille University's SSO system.
+
+ -
+ You agree to be bound by these Terms and Conditions, as well as any
+ additional rules, policies, or guidelines applicable to the use of
+ this website.
+
+
+
+ If you do not agree with these terms, you are not authorized to access
+ or use this website.
+
+ 2. Eligibility
+
+ Access to this website is restricted to authorized individuals
+ affiliated with Aix-Marseille University, such as students, faculty,
+ staff, or others explicitly granted access. Unauthorized use is strictly
+ prohibited and may result in suspension or termination of access.
+
+ 3. Authentication Through AMU SSO
+
+ -
+ Authentication through AMU's SSO system is required to access this
+ website.
+
+ -
+ You are responsible for safeguarding your AMU SSO login credentials
+ and ensuring they are not shared with others.
+
+ -
+ If you suspect unauthorized use of your AMU SSO credentials, you must
+ immediately notify Aix-Marseille University's IT services at{" "}
+
+ https://dirnum.univ-amu.fr/fr
+ .
+
+
+ 4. Permitted Use
+ By accessing the website, you agree to:
+
+ -
+ Use the website only for its intended academic, administrative, or
+ research purposes.
+
+ -
+ Refrain from engaging in any of the following prohibited activities:
+
+
+ -
+ Sharing your access credentials with unauthorized individuals.
+
+ -
+ Misusing, modifying, or attempting to exploit the website's
+ services.
+
+ -
+ Uploading or distributing malware, offensive content, or any
+ material that violates university policies or applicable laws.
+
+
+
+ 5. Privacy and Data Protection
+ By using this website:
+
+ -
+ You acknowledge that your personal data, including your AMU SSO login
+ and activity on the website, may be collected, processed, and stored
+ in accordance with Aix-Marseille University’s privacy policy and
+ applicable data protection laws (e.g., GDPR).
+
+ -
+ This data is used for authentication, and improving the website's
+ services.
+
+
+ 6. Intellectual Property
+
+ -
+ All content and materials provided on this website are the
+ intellectual property of Aix-Marseille University or its licensors.
+
+ -
+ You are granted a limited, non-transferable license to use the content
+ for personal, academic, or research purposes. Any unauthorized use,
+ reproduction, or distribution is strictly prohibited.
+
+
+ 7. Termination of Access
+
+ -
+ Aix-Marseille University reserves the right to suspend or terminate
+ your access without notice if:
+
+
+ -
+ You violate these Terms and Conditions or university policies.
+
+ -
+ Your AMU affiliation is revoked or your SSO account is deactivated.
+
+
+ -
+ Unauthorized access attempts may be reported to the appropriate
+ authorities.
+
+
+ 8. Disclaimers and Limitations of Liability
+
+ -
+ The website and its content are provided "as is" and "as available"
+ without any warranties, express or implied.
+
+ -
+ Aix-Marseille University and the website administrators are not liable
+ for:
+
+
+ - Interruptions in service, data loss, or technical issues.
+ -
+ Any unauthorized use of your AMU SSO credentials resulting from your
+ negligence.
+
+
+
+ 9. Modifications to the Terms
+
+ -
+ Aix-Marseille University may update these Terms and Conditions
+ periodically to reflect changes in laws, policies, or services.
+
+ -
+ Your continued use of the website following any changes constitutes
+ your acceptance of the updated terms.
+
+
+ 10. Governing Law
+
+ These Terms and Conditions are governed by and construed in accordance
+ with the laws of France and applicable EU regulations.
+
+ 11. Contact Information
+
+ For questions or support, please contact:{" "}
+
+ Aix-Marseille University IT Services
+ .
+
>
);
}
diff --git a/routes/index.tsx b/routes/index.tsx
index a210e9a..f879091 100644
--- a/routes/index.tsx
+++ b/routes/index.tsx
@@ -4,7 +4,8 @@ import { FreshContext } from "$fresh/server.ts";
export default async function Home(_request: Request, _context: FreshContext) {
return (
<>
- Welcome to PolyMPR!
+ PolyMPR
+ The ultimate HR platform
>
);
}
diff --git a/routes/login.tsx b/routes/login.tsx
index 66dfcb0..dbaa622 100644
--- a/routes/login.tsx
+++ b/routes/login.tsx
@@ -11,7 +11,6 @@ import { createJwt } from "@popov/jwt";
import { setCookie } from "$std/http/cookie.ts";
import { getKey } from "$root/routes/_middleware.ts";
-const SERVICE = "https://localhost/login";
const CAS = "https://ident.univ-amu.fr/cas";
function getTag(tag: CasTagNode): [string, string] {
@@ -57,10 +56,11 @@ export const handler: Handlers = {
async GET(request, context) {
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}`,
+ `${CAS}/serviceValidate?service=${service}&ticket=${ticket}`,
);
const body = parse(await response.text()) as [RegularTagNode];
const casResponse = body[0].children[0] as CasResponse;
@@ -69,7 +69,7 @@ export const handler: Handlers = {
return new Response(null, {
status: 302,
headers: {
- Location: `${CAS}/login?service=${SERVICE}`,
+ Location: `${CAS}/login?service=${service}`,
},
});
}
@@ -99,7 +99,7 @@ export const handler: Handlers = {
return new Response(null, {
status: 302,
headers: {
- Location: `${CAS}/login?service=${SERVICE}`,
+ Location: `${CAS}/login?service=${service}`,
},
});
}
diff --git a/routes/logout.tsx b/routes/logout.tsx
index 4481ac3..6111c62 100644
--- a/routes/logout.tsx
+++ b/routes/logout.tsx
@@ -2,7 +2,6 @@ import { Handlers } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts";
import { deleteCookie } from "$std/http/cookie.ts";
-const SERVICE = "https://localhost/";
const CAS = "https://ident.univ-amu.fr/cas";
// deno-lint-ignore no-explicit-any
@@ -12,7 +11,7 @@ export const handler: Handlers = {
const headers = new Headers();
deleteCookie(headers, "sessionToken", { path: "/" });
- headers.set("Location", `${CAS}/logout?service=${SERVICE}`);
+ headers.set("Location", `${CAS}/logout?service=${context.url.origin}`);
return new Response(null, {
status: 302,
From 596ee0536a5d2031e00c464be11b936982899ea1 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Tue, 21 Jan 2025 23:53:43 +0100
Subject: [PATCH 2/6] formatted code
---
routes/index.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/routes/index.tsx b/routes/index.tsx
index f879091..f92dc1b 100644
--- a/routes/index.tsx
+++ b/routes/index.tsx
@@ -5,7 +5,9 @@ export default async function Home(_request: Request, _context: FreshContext) {
return (
<>
PolyMPR
- The ultimate HR platform
+
+ The ultimate HR platform
+
>
);
}
From 3ce127345568fe36c0f45988a83140fbc097db41 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Wed, 22 Jan 2025 00:54:43 +0100
Subject: [PATCH 3/6] Started documenting code
---
databases/ensure.ts | 10 +++++++++-
defaults/interfaces.ts | 9 +++++++++
defaults/makeIndex.ts | 18 ++++++++++++++----
defaults/makePartials.tsx | 27 ++++++++++++++++++++-------
routes/(apps)/_layout.tsx | 2 +-
routes/_app.tsx | 2 +-
routes/_middleware.ts | 24 ++++++++++++++++--------
routes/login.tsx | 2 +-
routes/logout.tsx | 2 +-
9 files changed, 72 insertions(+), 24 deletions(-)
diff --git a/databases/ensure.ts b/databases/ensure.ts
index 1c56375..c1cde16 100644
--- a/databases/ensure.ts
+++ b/databases/ensure.ts
@@ -1,6 +1,14 @@
import { Database } from "@db/sqlite";
-export default async function ensureDatabases() {
+/**
+ * Ensure database file creation on new server start.
+ *
+ * Read all SQL files in init directory and create
+ * associated SQLite database file.
+ *
+ * **Must not be used out of statup use-case.**
+ */
+export default async function ensureDatabases(): Promise {
await Deno.mkdir("databases/data", { recursive: true });
for await (const file of Deno.readDir("databases/init")) {
diff --git a/defaults/interfaces.ts b/defaults/interfaces.ts
index 3189dfe..24a5076 100644
--- a/defaults/interfaces.ts
+++ b/defaults/interfaces.ts
@@ -1,4 +1,10 @@
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
+import { AsyncRoute } from "$fresh/src/server/types.ts";
+
+export interface State {
+ isAuthenticated: boolean;
+ session: CasContent;
+}
export interface AppProperties {
name: string;
@@ -48,3 +54,6 @@ export interface LoginJWT {
}
export type EmptyObject = Record;
+
+// deno-lint-ignore no-explicit-any
+export type Route = AsyncRoute;
\ No newline at end of file
diff --git a/defaults/makeIndex.ts b/defaults/makeIndex.ts
index f4ebd11..19ebc91 100644
--- a/defaults/makeIndex.ts
+++ b/defaults/makeIndex.ts
@@ -1,12 +1,22 @@
import { FreshContext } from "$fresh/server.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { Route, State } from "$root/defaults/interfaces.ts";
+import { ComponentChildren } from "preact";
-export default function makeIndex(basePath: string) {
+/**
+ * Generates index file based on `Index` fresh partial to avoid code duplication.
+ * @param basePath The base path of the module, should be `import.meta.url!`.
+ * @returns The `Index` fresh partial that will be displayed by default.
+ *
+ * @example
+ * import makeIndex from "$root/defaults/makeIndex.ts";
+ * export default makeIndex(import.meta.dirname!);
+ */
+export default function makeIndex(basePath: string): Route {
return async function Index(
request: Request,
context: FreshContext,
- ) {
- const index = (await import(`${basePath}/partials/index.tsx`)).Index;
+ ): Promise {
+ const index: Route = (await import(`${basePath}/partials/index.tsx`)).Index;
return index(request, context);
};
}
diff --git a/defaults/makePartials.tsx b/defaults/makePartials.tsx
index e6b81af..5df6916 100644
--- a/defaults/makePartials.tsx
+++ b/defaults/makePartials.tsx
@@ -1,8 +1,12 @@
import { JSX } from "preact";
import { Partial } from "$fresh/runtime.ts";
import { FreshContext, RouteConfig } from "$fresh/server.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { Route, State } from "$root/defaults/interfaces.ts";
+/**
+ * Gets the `RouteConfig` config object for partial pages.
+ * @returns The partials config object.
+ */
export function getPartialsConfig(): RouteConfig {
return {
skipAppWrapper: true,
@@ -10,12 +14,21 @@ export function getPartialsConfig(): RouteConfig {
};
}
-export function makePartials(
- page: (
- request: Request,
- context: FreshContext,
- ) => Promise,
-) {
+/**
+ * Partialize the given page for optimized rendering.
+ * @param page The partial `Route` object to partialize.
+ * @returns The partialized version of `page`.
+ * @example
+ * // Page defintion...
+ * async function Page(_request: Request, context: FreshContext) {
+ * return My super page!
;
+ * }
+ *
+ * // Partial code that should be at each file's end.
+ * export const config = getPartialsConfig();
+ * export default makePartials(Page);
+ */
+export function makePartials(page: Route) {
return async function WrappedElements(
request: Request,
context: FreshContext,
diff --git a/routes/(apps)/_layout.tsx b/routes/(apps)/_layout.tsx
index 0fe56fa..ee3b43e 100644
--- a/routes/(apps)/_layout.tsx
+++ b/routes/(apps)/_layout.tsx
@@ -1,6 +1,6 @@
import { FreshContext } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { State } from "$root/defaults/interfaces.ts";
import { AppProperties } from "$root/defaults/interfaces.ts";
import Navbar from "$root/routes/(_islands)/Navbar.tsx";
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 7025b97..8b253b8 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -1,5 +1,5 @@
import { FreshContext } from "$fresh/server.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { State } from "$root/defaults/interfaces.ts";
import Header from "$root/routes/(_components)/Header.tsx";
import Footer from "$root/routes/(_components)/Footer.tsx";
diff --git a/routes/_middleware.ts b/routes/_middleware.ts
index 3601da1..fbaca27 100644
--- a/routes/_middleware.ts
+++ b/routes/_middleware.ts
@@ -1,7 +1,7 @@
import { FreshContext } from "$fresh/server.ts";
import { getCookies } from "$std/http/cookie.ts";
import { getJwtPayload, isJwtValid } from "@popov/jwt";
-import { CasContent, LoginJWT } from "$root/defaults/interfaces.ts";
+import { CasContent, LoginJWT, State } from "$root/defaults/interfaces.ts";
const PUBLIC_ROUTES = [
"/",
@@ -13,21 +13,29 @@ const PUBLIC_ROUTES = [
];
const jwtKeyCache: Record = {};
+const deleteKey = (user: string) => delete jwtKeyCache[user];
-export interface State {
- isAuthenticated: boolean;
- session: CasContent;
-}
-
-function isRoutePublic(route: string) {
- return PUBLIC_ROUTES.includes(route) || route.match(/\..+$/);
+/**
+ * Checks if the given route is public.
+ * @param route The route to check.
+ * @returns `true` if the route is public, `false` otherwise.
+ */
+function isRoutePublic(route: string): boolean {
+ return PUBLIC_ROUTES.includes(route) ||
+ !!(route.match(/\..+$/)?.[0] ?? false);
}
+/**
+ * Get the given user's key, creating it if not already existing.
+ * @param user The key's user.
+ * @returns The user's key.
+ */
export function getKey(user: string): string {
if (!jwtKeyCache[user]) {
const keyBuffer = new Uint8Array(32);
crypto.getRandomValues(keyBuffer);
jwtKeyCache[user] = new TextDecoder().decode(keyBuffer);
+ setTimeout(deleteKey, 0x75300, user);
}
return jwtKeyCache[user];
diff --git a/routes/login.tsx b/routes/login.tsx
index dbaa622..4bbd1c7 100644
--- a/routes/login.tsx
+++ b/routes/login.tsx
@@ -1,5 +1,5 @@
import { Handlers } from "$fresh/server.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { State } from "$root/defaults/interfaces.ts";
import { parse, type RegularTagNode } from "@melvdouc/xml-parser";
import {
CasContent,
diff --git a/routes/logout.tsx b/routes/logout.tsx
index 6111c62..8ebc21b 100644
--- a/routes/logout.tsx
+++ b/routes/logout.tsx
@@ -1,5 +1,5 @@
import { Handlers } from "$fresh/server.ts";
-import { State } from "$root/routes/_middleware.ts";
+import { State } from "$root/defaults/interfaces.ts";
import { deleteCookie } from "$std/http/cookie.ts";
const CAS = "https://ident.univ-amu.fr/cas";
From 8a5461827edb9e37a8bda886141643794a962d37 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Wed, 22 Jan 2025 11:15:43 +0100
Subject: [PATCH 4/6] Optimized code and wrote documentation
---
defaults/interfaces.ts | 13 ++++--
routes/_middleware.ts | 29 +++++++++----
routes/apps.tsx | 27 ++++++++++---
routes/login.tsx | 92 +++++++++++++++++++++++-------------------
routes/logout.tsx | 13 ++++--
5 files changed, 113 insertions(+), 61 deletions(-)
diff --git a/defaults/interfaces.ts b/defaults/interfaces.ts
index 24a5076..a5a6268 100644
--- a/defaults/interfaces.ts
+++ b/defaults/interfaces.ts
@@ -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;
@@ -56,4 +63,4 @@ export interface LoginJWT {
export type EmptyObject = Record;
// deno-lint-ignore no-explicit-any
-export type Route = AsyncRoute;
\ No newline at end of file
+export type Route = AsyncRoute;
diff --git a/routes/_middleware.ts b/routes/_middleware.ts
index fbaca27..01b449e 100644
--- a/routes/_middleware.ts
+++ b/routes/_middleware.ts
@@ -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[] = [
+ /**
+ * 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,
- ) {
+ ): Promise {
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,
- ) {
+ ): Promise {
const url = new URL(request.url);
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) {
diff --git a/routes/apps.tsx b/routes/apps.tsx
index 54c4e44..d64cabb 100644
--- a/routes/apps.tsx
+++ b/routes/apps.tsx
@@ -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 = {};
+const apps: Record = {};
+
+export const handler: Handlers, 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>,
+ ): Promise {
+ 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>,
+) {
return (
<>
diff --git a/routes/login.tsx b/routes/login.tsx
index 4bbd1c7..3b1da1e 100644
--- a/routes/login.tsx
+++ b/routes/login.tsx
@@ -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 {
const nodes = casResponse.children[1].children.map(getTag);
const fullUserInfos: Record = {};
@@ -28,7 +38,6 @@ function createUserJWT(casResponse: CasResponse): Promise {
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 {
});
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 {
return createJwt(payload, key);
}
-// deno-lint-ignore no-explicit-any
-export const handler: Handlers = {
- async GET(request, context) {
+export const handler: Handlers = {
+ /**
+ * 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,
+ ): Promise {
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 = {
},
});
}
+
+ const headers = new Headers();
+
+ setCookie(headers, {
+ name: "sessionToken",
+ value: await createUserJWT(casResponse),
+ });
+ headers.set("Location", "/apps");
+
+ return new Response(null, {
+ status: 302,
+ headers,
+ });
},
};
diff --git a/routes/logout.tsx b/routes/logout.tsx
index 8ebc21b..1547942 100644
--- a/routes/logout.tsx
+++ b/routes/logout.tsx
@@ -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 = {
- GET(_request, context) {
+export const handler: Handlers = {
+ /**
+ * 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): Response {
if (context.state.isAuthenticated) {
const headers = new Headers();
From 75a9591f6af923d8a125331ff6ded585042f26b8 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Wed, 22 Jan 2025 14:24:55 +0100
Subject: [PATCH 5/6] Added documentation on cli
---
databases/ensure.ts | 2 +-
defaults/makePartials.tsx | 4 +-
toolbox/cli.ts | 101 ++++----------------------------------
toolbox/cli/command.ts | 40 +++++++++++++++
toolbox/cli/help.ts | 70 ++++++++++++++++++++++++++
toolbox/cli/main.ts | 14 ++++++
toolbox/module/create.ts | 35 +++++++++++++
toolbox/module/list.ts | 3 ++
8 files changed, 175 insertions(+), 94 deletions(-)
create mode 100644 toolbox/cli/command.ts
create mode 100644 toolbox/cli/help.ts
create mode 100644 toolbox/cli/main.ts
diff --git a/databases/ensure.ts b/databases/ensure.ts
index c1cde16..41431f5 100644
--- a/databases/ensure.ts
+++ b/databases/ensure.ts
@@ -5,7 +5,7 @@ import { Database } from "@db/sqlite";
*
* Read all SQL files in init directory and create
* associated SQLite database file.
- *
+ *
* **Must not be used out of statup use-case.**
*/
export default async function ensureDatabases(): Promise {
diff --git a/defaults/makePartials.tsx b/defaults/makePartials.tsx
index 5df6916..90dc7cf 100644
--- a/defaults/makePartials.tsx
+++ b/defaults/makePartials.tsx
@@ -16,14 +16,14 @@ export function getPartialsConfig(): RouteConfig {
/**
* Partialize the given page for optimized rendering.
- * @param page The partial `Route` object to partialize.
+ * @param page The partial `Route` object to partialize.
* @returns The partialized version of `page`.
* @example
* // Page defintion...
* async function Page(_request: Request, context: FreshContext) {
* return My super page!
;
* }
- *
+ *
* // Partial code that should be at each file's end.
* export const config = getPartialsConfig();
* export default makePartials(Page);
diff --git a/toolbox/cli.ts b/toolbox/cli.ts
index 91f91f1..015616b 100644
--- a/toolbox/cli.ts
+++ b/toolbox/cli.ts
@@ -1,15 +1,17 @@
-import { parseArgs, type ParseOptions } from "@std/cli/parse-args";
+import { type ParseOptions } from "@std/cli/parse-args";
import { createModule } from "$root/toolbox/module/create.ts";
import { listModules } from "$root/toolbox/module/list.ts";
+import { CLI, displayHelp } from "$root/toolbox/cli/help.ts";
+import { main } from "$root/toolbox/cli/main.ts";
-type CLICommand = (...args: Array) => void;
-type CLIAsyncCommand = (...args: Array) => Promise;
-interface CLI {
- [command: string]: CLI | CLICommand | CLIAsyncCommand;
-}
-
+/**
+ * CLI will use `args._`, but you can define options for global CLI.
+ */
const argSpec: ParseOptions = {};
+/**
+ * Configure CLI commands here.
+ */
const cli: CLI = {
help: () => displayHelp(cli),
module: {
@@ -18,87 +20,4 @@ const cli: CLI = {
},
};
-function displayHelp(
- cli: CLI | CLICommand | CLIAsyncCommand,
- errorMessage?: string,
-): never {
- const loggingFunction = errorMessage ? console.error : console.log;
- if (errorMessage) {
- console.error(errorMessage);
- }
-
- if (typeof cli == "function") {
- loggingFunction("Usage:");
- displayFunctionHelp(cli, loggingFunction);
- } else {
- loggingFunction("Commands:");
- displayObjectHelp(cli, loggingFunction);
- }
-
- Deno.exit(errorMessage ? 1 : 0);
-}
-
-function displayObjectHelp(
- cli: CLI,
- loggingFunction: typeof console.log,
- level: number = 1,
-) {
- for (const [key, value] of Object.entries(cli)) {
- if (typeof value == "function") {
- displayFunctionHelp(value, loggingFunction, level);
- } else {
- loggingFunction(`${" ".repeat(level * 2)}${key}`);
- displayObjectHelp(value, loggingFunction, level + 1);
- }
- }
-}
-
-function displayFunctionHelp(
- cli: CLICommand | CLIAsyncCommand,
- loggingFunction: typeof console.log,
- level: number = 1,
-) {
- const command = cli.name;
- const matched = cli.toString().match(/(?<=^\()[^\)]+(?=\))/);
- const args = matched?.[0].split(",").map((arg) => `<${arg.trim()}>`);
- loggingFunction(
- `${" ".repeat(level * 2)}${command} ${args?.join(" ") ?? ""}`,
- );
-}
-
-function runCommand(
- commands: Array,
- cli: CLI,
-): never | {
- command: CLICommand | CLIAsyncCommand;
- args: Array;
-} {
- if (commands.length == 0) {
- displayHelp(cli, `No command provided.`);
- }
-
- const command = commands.shift()!.toString();
-
- if (cli[command] == undefined) {
- displayHelp(cli, `Command "${command}" doesn't exist.`);
- }
-
- if (typeof cli[command] == "object") {
- return runCommand(commands, cli[command]);
- }
-
- if (cli[command].length != commands.length) {
- displayHelp(cli[command], `Wrong usage of command "${command}".`);
- }
-
- return { command: cli[command], args: commands };
-}
-
-function main() {
- const argv = parseArgs(Deno.args, argSpec);
- const { command, args } = runCommand(argv._, cli);
-
- command(...args.map((element) => element.toString()));
-}
-
-main();
+main(cli, argSpec);
diff --git a/toolbox/cli/command.ts b/toolbox/cli/command.ts
new file mode 100644
index 0000000..d9dc830
--- /dev/null
+++ b/toolbox/cli/command.ts
@@ -0,0 +1,40 @@
+import {
+ CLI,
+ CLIAsyncCommand,
+ CLICommand,
+ displayHelp,
+} from "$root/toolbox/cli/help.ts";
+
+/**
+ * Run the command given by arguments.
+ * @param commands The given arguments.
+ * @param cli The CLI (sub-)configuration object.
+ * @returns The command or the help if commands are not valid.
+ */
+export function runCommand(
+ commands: Array,
+ cli: CLI,
+): never | {
+ command: CLICommand | CLIAsyncCommand;
+ args: Array;
+} {
+ if (commands.length == 0) {
+ displayHelp(cli, `No command provided.`);
+ }
+
+ const command = commands.shift()!.toString();
+
+ if (cli[command] == undefined) {
+ displayHelp(cli, `Command "${command}" doesn't exist.`);
+ }
+
+ if (typeof cli[command] == "object") {
+ return runCommand(commands, cli[command]);
+ }
+
+ if (cli[command].length != commands.length) {
+ displayHelp(cli[command], `Wrong usage of command "${command}".`);
+ }
+
+ return { command: cli[command], args: commands };
+}
diff --git a/toolbox/cli/help.ts b/toolbox/cli/help.ts
new file mode 100644
index 0000000..b906dd5
--- /dev/null
+++ b/toolbox/cli/help.ts
@@ -0,0 +1,70 @@
+export type CLICommand = (...args: Array) => void;
+export type CLIAsyncCommand = (...args: Array) => Promise;
+export interface CLI {
+ [command: string]: CLI | CLICommand | CLIAsyncCommand;
+}
+
+/**
+ * Display the help message for the CLI.
+ * @param cli The CLI (sub-)configuration object.
+ * @param errorMessage The error message to display.
+ */
+export function displayHelp(
+ cli: CLI | CLICommand | CLIAsyncCommand,
+ errorMessage?: string,
+): never {
+ const loggingFunction = errorMessage ? console.error : console.log;
+ if (errorMessage) {
+ console.error(errorMessage);
+ }
+
+ if (typeof cli == "function") {
+ loggingFunction("Usage:");
+ displayFunctionHelp(cli, loggingFunction);
+ } else {
+ loggingFunction("Commands:");
+ displayObjectHelp(cli, loggingFunction);
+ }
+
+ Deno.exit(errorMessage ? 1 : 0);
+}
+
+/**
+ * Display object help recursivelly.
+ * @param cli The CLI (sub-)configuration object.
+ * @param loggingFunction The logging function to use.
+ * @param level The tab level.
+ */
+function displayObjectHelp(
+ cli: CLI,
+ loggingFunction: typeof console.log,
+ level: number = 1,
+) {
+ for (const [key, value] of Object.entries(cli)) {
+ if (typeof value == "function") {
+ displayFunctionHelp(value, loggingFunction, level);
+ } else {
+ loggingFunction(`${" ".repeat(level * 2)}${key}`);
+ displayObjectHelp(value, loggingFunction, level + 1);
+ }
+ }
+}
+
+/**
+ * Display function help.
+ * @param cli The CLI function.
+ * @param loggingFunction The logging function to use.
+ * @param level The tab level.
+ */
+function displayFunctionHelp(
+ cli: CLICommand | CLIAsyncCommand,
+ loggingFunction: typeof console.log,
+ level: number = 1,
+) {
+ const command = cli.name;
+ const matched = cli.toString().match(/(?<=^\()[^\)]+(?=\))/);
+ const args = matched?.[0].split(",").map((arg) => `<${arg.trim()}>`);
+ loggingFunction(
+ `${" ".repeat(level * 2)}${command} ${args?.join(" ") ?? ""}`,
+ );
+}
diff --git a/toolbox/cli/main.ts b/toolbox/cli/main.ts
new file mode 100644
index 0000000..7e57c63
--- /dev/null
+++ b/toolbox/cli/main.ts
@@ -0,0 +1,14 @@
+import { parseArgs, ParseOptions } from "@std/cli/parse-args";
+import { runCommand } from "$root/toolbox/cli/command.ts";
+import { CLI } from "$root/toolbox/cli/help.ts";
+
+/**
+ * Runs the CLI.
+ * @param cli The CLI configuration object.
+ * @param argSpec The Parse options for args.
+ */
+export function main(cli: CLI, argSpec: ParseOptions) {
+ const argv = parseArgs(Deno.args, argSpec);
+ const { command, args } = runCommand(argv._, cli);
+ command(...args.map((element) => element.toString()));
+}
diff --git a/toolbox/module/create.ts b/toolbox/module/create.ts
index aa93463..1647665 100644
--- a/toolbox/module/create.ts
+++ b/toolbox/module/create.ts
@@ -1,3 +1,7 @@
+/**
+ * Creates a new module.
+ * @param name The module name.
+ */
export async function createModule(name: string): Promise {
if (!name.match(/^[a-zA-Z0-9](?:(?:\-(?!\-))?[a-zA-Z0-9]*)*[a-zA-Z0-9]$/)) {
console.error("Module names must be in kebab case.");
@@ -58,16 +62,32 @@ export async function createModule(name: string): Promise {
formatter.output();
}
+/**
+ * Creates a new file at given path.
+ * @param path The file path.
+ * @param content The file content.
+ * @returns The creation promise.
+ */
function createFile(path: string, content: string): Promise {
console.log(`Creating file ${path}...`);
return Deno.writeTextFile(path, content);
}
+/**
+ * Creates a new directory at given path.
+ * @param path The directory path.
+ * @returns The creation promise.
+ */
function createDir(path: string): Promise {
console.log(`Creating directory ${path}...`);
return Deno.mkdir(path);
}
+/**
+ * Create the index content.
+ * @param _name The module capitalized name.
+ * @returns The index content.
+ */
function getIndexContent(_name: string) {
return `
import makeIndex from "$root/defaults/makeIndex.ts";
@@ -75,6 +95,11 @@ function getIndexContent(_name: string) {
`;
}
+/**
+ * Create the partials index content.
+ * @param name The module capitalized name.
+ * @returns The partials index content.
+ */
function getPartialIndexContent(name: string) {
return `
import {
@@ -93,6 +118,11 @@ function getPartialIndexContent(name: string) {
`;
}
+/**
+ * Create the props content.
+ * @param name The module capitalized name.
+ * @returns The props content.
+ */
function getPropsContent(name: string) {
return `
import { AppProperties } from "$root/defaults/interfaces.ts";
@@ -111,6 +141,11 @@ function getPropsContent(name: string) {
`;
}
+/**
+ * Create the API example content.
+ * @param _name The module capitalized name.
+ * @returns The API example content.
+ */
function getApiExampleContent(_name: string) {
return `
import { Handlers } from "$fresh/server.ts";
diff --git a/toolbox/module/list.ts b/toolbox/module/list.ts
index a348047..258e1a5 100644
--- a/toolbox/module/list.ts
+++ b/toolbox/module/list.ts
@@ -1,3 +1,6 @@
+/**
+ * List all modules of PolyMPR.
+ */
export async function listModules(): Promise {
for await (const path of Deno.readDir("routes/(apps)")) {
if (path.isDirectory) {
From 5464077debc6b0fa0fb96ffbaf32a7c18060b662 Mon Sep 17 00:00:00 2001
From: Kevin FEDYNA
Date: Wed, 22 Jan 2025 14:32:51 +0100
Subject: [PATCH 6/6] Added compile task
---
deno.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/deno.json b/deno.json
index 344187a..a7635bb 100644
--- a/deno.json
+++ b/deno.json
@@ -4,6 +4,7 @@
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
"pmpr": "deno run -A toolbox/cli.ts",
+ "compile": "deno compile -A --output \"/home/$USER/.deno/bin/pmpr\" toolbox/cli.ts",
"manifest": "deno task cli manifest $(pwd)",
"start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts",
"build": "deno run -A --unstable-ffi dev.ts build",