Merge branch 'main' into PMPR-27
This commit is contained in:
+9
-1
@@ -1,6 +1,14 @@
|
|||||||
import { Database } from "@db/sqlite";
|
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<void> {
|
||||||
await Deno.mkdir("databases/data", { recursive: true });
|
await Deno.mkdir("databases/data", { recursive: true });
|
||||||
|
|
||||||
for await (const file of Deno.readDir("databases/init")) {
|
for await (const file of Deno.readDir("databases/init")) {
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
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";
|
||||||
|
|
||||||
|
interface AuthenticatedState {
|
||||||
|
isAuthenticated: true;
|
||||||
|
session: CasContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnauthenticatedState {
|
||||||
|
isAuthenticated: false;
|
||||||
|
session: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = AuthenticatedState | UnauthenticatedState;
|
||||||
|
|
||||||
export interface AppProperties {
|
export interface AppProperties {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,3 +61,6 @@ export interface LoginJWT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EmptyObject = Record<string | number | symbol, never>;
|
export type EmptyObject = Record<string | number | symbol, never>;
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
export type Route = AsyncRoute<any, State>;
|
||||||
|
|||||||
+14
-4
@@ -1,12 +1,22 @@
|
|||||||
import { FreshContext } from "$fresh/server.ts";
|
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(
|
return async function Index(
|
||||||
request: Request,
|
request: Request,
|
||||||
context: FreshContext<State>,
|
context: FreshContext<State>,
|
||||||
) {
|
): Promise<ComponentChildren | Response> {
|
||||||
const index = (await import(`${basePath}/partials/index.tsx`)).Index;
|
const index: Route = (await import(`${basePath}/partials/index.tsx`)).Index;
|
||||||
return index(request, context);
|
return index(request, context);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { JSX } from "preact";
|
import { JSX } from "preact";
|
||||||
import { Partial } from "$fresh/runtime.ts";
|
import { Partial } from "$fresh/runtime.ts";
|
||||||
import { FreshContext, RouteConfig } from "$fresh/server.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 {
|
export function getPartialsConfig(): RouteConfig {
|
||||||
return {
|
return {
|
||||||
skipAppWrapper: true,
|
skipAppWrapper: true,
|
||||||
@@ -10,12 +14,21 @@ export function getPartialsConfig(): RouteConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makePartials(
|
/**
|
||||||
page: (
|
* Partialize the given page for optimized rendering.
|
||||||
request: Request,
|
* @param page The partial `Route` object to partialize.
|
||||||
context: FreshContext<State>,
|
* @returns The partialized version of `page`.
|
||||||
) => Promise<JSX.Element>,
|
* @example
|
||||||
) {
|
* // Page defintion...
|
||||||
|
* async function Page(_request: Request, context: FreshContext<State>) {
|
||||||
|
* return <h2>My super page!</h2>;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // 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(
|
return async function WrappedElements(
|
||||||
request: Request,
|
request: Request,
|
||||||
context: FreshContext<State>,
|
context: FreshContext<State>,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
||||||
"pmpr": "deno run -A toolbox/cli.ts",
|
"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)",
|
"manifest": "deno task cli manifest $(pwd)",
|
||||||
"start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts",
|
"start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts",
|
||||||
"build": "deno run -A --unstable-ffi dev.ts build",
|
"build": "deno run -A --unstable-ffi dev.ts build",
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partia
|
|||||||
import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx";
|
import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx";
|
||||||
import * as $_apps_notes_index from "./routes/(apps)/notes/index.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_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_index from "./routes/(apps)/notes/partials/index.tsx";
|
||||||
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
|
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
|
||||||
import * as $_apps_students_api_insert_students from "./routes/(apps)/students/api/insert_students.ts";
|
import * as $_apps_students_api_insert_students from "./routes/(apps)/students/api/insert_students.ts";
|
||||||
@@ -52,8 +51,6 @@ const manifest = {
|
|||||||
"./routes/(apps)/notes/index.tsx": $_apps_notes_index,
|
"./routes/(apps)/notes/index.tsx": $_apps_notes_index,
|
||||||
"./routes/(apps)/notes/partials/(admin)/courses.tsx":
|
"./routes/(apps)/notes/partials/(admin)/courses.tsx":
|
||||||
$_apps_notes_partials_admin_courses,
|
$_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/index.tsx": $_apps_notes_partials_index,
|
||||||
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
|
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
|
||||||
"./routes/(apps)/students/api/insert_students.ts":
|
"./routes/(apps)/students/api/insert_students.ts":
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FreshContext } from "$fresh/server.ts";
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
import { Partial } from "$fresh/runtime.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 { AppProperties } from "$root/defaults/interfaces.ts";
|
||||||
import Navbar from "$root/routes/(_islands)/Navbar.tsx";
|
import Navbar from "$root/routes/(_islands)/Navbar.tsx";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const properties: AppProperties = {
|
|||||||
index: "Homepage",
|
index: "Homepage",
|
||||||
notes: "Notes",
|
notes: "Notes",
|
||||||
courses: "Courses management",
|
courses: "Courses management",
|
||||||
students: "Students management",
|
|
||||||
},
|
},
|
||||||
adminOnly: ["courses", "students"],
|
adminOnly: ["courses", "students"],
|
||||||
hint: "Student grading management",
|
hint: "Student grading management",
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { Partial } from "$fresh/runtime.ts";
|
import {
|
||||||
import { RouteConfig } from "$fresh/server.ts";
|
getPartialsConfig,
|
||||||
|
makePartials,
|
||||||
|
} from "$root/defaults/makePartials.tsx";
|
||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { State } from "$root/routes/_middleware.ts";
|
||||||
|
|
||||||
type ModulesProps = Record<string | number | symbol, never>;
|
// deno-lint-ignore require-await
|
||||||
|
async function Courses(_request: Request, context: FreshContext<State>) {
|
||||||
export const config: RouteConfig = {
|
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
|
||||||
skipAppWrapper: true,
|
|
||||||
skipInheritedLayouts: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Modules(_props: ModulesProps) {
|
|
||||||
return (
|
|
||||||
<Partial name="body">
|
|
||||||
<a href="notes" f-partial={"notes/partials"}>notes</a>
|
|
||||||
</Partial>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const config = getPartialsConfig();
|
||||||
|
export default makePartials(Courses);
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Partial } from "$fresh/runtime.ts";
|
|
||||||
import { RouteConfig } from "$fresh/server.ts";
|
|
||||||
|
|
||||||
type ModulesProps = Record<string | number | symbol, never>;
|
|
||||||
|
|
||||||
export const config: RouteConfig = {
|
|
||||||
skipAppWrapper: true,
|
|
||||||
skipInheritedLayouts: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Modules(_props: ModulesProps) {
|
|
||||||
return (
|
|
||||||
<Partial name="body">
|
|
||||||
<a href="notes" f-partial={"notes/partials"}>notes</a>
|
|
||||||
</Partial>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,12 @@ import {
|
|||||||
getPartialsConfig,
|
getPartialsConfig,
|
||||||
makePartials,
|
makePartials,
|
||||||
} from "$root/defaults/makePartials.tsx";
|
} from "$root/defaults/makePartials.tsx";
|
||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { State } from "$root/routes/_middleware.ts";
|
||||||
|
|
||||||
type NotesIndexProps = Record<string | number | symbol, never>;
|
// deno-lint-ignore require-await
|
||||||
|
export async function Index(_request: Request, context: FreshContext<State>) {
|
||||||
export function Index(_props: NotesIndexProps) {
|
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
|
||||||
return <a href="notes" f-partial={"notes/partials"}>bip boup</a>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = getPartialsConfig();
|
export const config = getPartialsConfig();
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { Partial } from "$fresh/runtime.ts";
|
import {
|
||||||
import { RouteConfig } from "$fresh/server.ts";
|
getPartialsConfig,
|
||||||
|
makePartials,
|
||||||
|
} from "$root/defaults/makePartials.tsx";
|
||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { State } from "$root/routes/_middleware.ts";
|
||||||
|
|
||||||
type ModulesProps = Record<string | number | symbol, never>;
|
// deno-lint-ignore require-await
|
||||||
|
async function Notes(_request: Request, context: FreshContext<State>) {
|
||||||
export const config: RouteConfig = {
|
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
|
||||||
skipAppWrapper: true,
|
|
||||||
skipInheritedLayouts: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Modules(_props: ModulesProps) {
|
|
||||||
return (
|
|
||||||
<Partial name="body">
|
|
||||||
<a href="notes" f-partial={"notes/partials"}>notes</a>
|
|
||||||
</Partial>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const config = getPartialsConfig();
|
||||||
|
export default makePartials(Notes);
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { FreshContext } from "$fresh/server.ts";
|
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 Header from "$root/routes/(_components)/Header.tsx";
|
||||||
import Footer from "$root/routes/(_components)/Footer.tsx";
|
import Footer from "$root/routes/(_components)/Footer.tsx";
|
||||||
|
|
||||||
|
|||||||
+38
-15
@@ -1,7 +1,7 @@
|
|||||||
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 } from "$root/defaults/interfaces.ts";
|
import { CasContent, LoginJWT, State } from "$root/defaults/interfaces.ts";
|
||||||
|
|
||||||
const PUBLIC_ROUTES = [
|
const PUBLIC_ROUTES = [
|
||||||
"/",
|
"/",
|
||||||
@@ -13,31 +13,45 @@ const PUBLIC_ROUTES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const jwtKeyCache: Record<string, string> = {};
|
const jwtKeyCache: Record<string, string> = {};
|
||||||
|
const deleteKey = (user: string) => delete jwtKeyCache[user];
|
||||||
|
|
||||||
export interface State {
|
/**
|
||||||
isAuthenticated: boolean;
|
* Checks if the given route is public.
|
||||||
session: CasContent;
|
* @param route The route to check.
|
||||||
}
|
* @returns `true` if the route is public, `false` otherwise.
|
||||||
|
*/
|
||||||
function isRoutePublic(route: string) {
|
function isRoutePublic(route: string): boolean {
|
||||||
return PUBLIC_ROUTES.includes(route) || route.match(/\..+$/);
|
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 {
|
export function getKey(user: string): string {
|
||||||
if (!jwtKeyCache[user]) {
|
if (!jwtKeyCache[user]) {
|
||||||
const keyBuffer = new Uint8Array(32);
|
const keyBuffer = new Uint8Array(32);
|
||||||
crypto.getRandomValues(keyBuffer);
|
crypto.getRandomValues(keyBuffer);
|
||||||
jwtKeyCache[user] = new TextDecoder().decode(keyBuffer);
|
jwtKeyCache[user] = new TextDecoder().decode(keyBuffer);
|
||||||
|
setTimeout(deleteKey, 0x75300, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -51,17 +65,26 @@ export const handler = [
|
|||||||
cookies["sessionToken"],
|
cookies["sessionToken"],
|
||||||
key,
|
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();
|
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) {
|
||||||
|
|||||||
+163
-1
@@ -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
|
PolyMPR is born from the will to enhance Polytech INFO department's HR
|
||||||
infrastructure.
|
infrastructure.
|
||||||
</p>
|
</p>
|
||||||
<h3>Terms of Use</h3>
|
<h2>Terms of Use</h2>
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
Last updated: 21<sup>th</sup> Jan. 2025
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<h3>1. Acceptance of Terms</h3>
|
||||||
|
<p>By logging in with your AMU SSO credentials, you confirm that:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
You are an authorized user of Aix-Marseille University's SSO system.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
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.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you do not agree with these terms, you are not authorized to access
|
||||||
|
or use this website.
|
||||||
|
</p>
|
||||||
|
<h3>2. Eligibility</h3>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<h3>3. Authentication Through AMU SSO</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Authentication through AMU's SSO system is required to access this
|
||||||
|
website.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You are responsible for safeguarding your AMU SSO login credentials
|
||||||
|
and ensuring they are not shared with others.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If you suspect unauthorized use of your AMU SSO credentials, you must
|
||||||
|
immediately notify Aix-Marseille University's IT services at{" "}
|
||||||
|
<a href="https://dirnum.univ-amu.fr/fr">
|
||||||
|
https://dirnum.univ-amu.fr/fr
|
||||||
|
</a>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3>4. Permitted Use</h3>
|
||||||
|
<p>By accessing the website, you agree to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Use the website only for its intended academic, administrative, or
|
||||||
|
research purposes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Refrain from engaging in any of the following prohibited activities:
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Sharing your access credentials with unauthorized individuals.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Misusing, modifying, or attempting to exploit the website's
|
||||||
|
services.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Uploading or distributing malware, offensive content, or any
|
||||||
|
material that violates university policies or applicable laws.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
<h3>5. Privacy and Data Protection</h3>
|
||||||
|
<p>By using this website:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
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).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
This data is used for authentication, and improving the website's
|
||||||
|
services.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3>6. Intellectual Property</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
All content and materials provided on this website are the
|
||||||
|
intellectual property of Aix-Marseille University or its licensors.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
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.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3>7. Termination of Access</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Aix-Marseille University reserves the right to suspend or terminate
|
||||||
|
your access without notice if:
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
You violate these Terms and Conditions or university policies.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Your AMU affiliation is revoked or your SSO account is deactivated.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<li>
|
||||||
|
Unauthorized access attempts may be reported to the appropriate
|
||||||
|
authorities.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3>8. Disclaimers and Limitations of Liability</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
The website and its content are provided "as is" and "as available"
|
||||||
|
without any warranties, express or implied.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Aix-Marseille University and the website administrators are not liable
|
||||||
|
for:
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
<li>Interruptions in service, data loss, or technical issues.</li>
|
||||||
|
<li>
|
||||||
|
Any unauthorized use of your AMU SSO credentials resulting from your
|
||||||
|
negligence.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
<h3>9. Modifications to the Terms</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Aix-Marseille University may update these Terms and Conditions
|
||||||
|
periodically to reflect changes in laws, policies, or services.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Your continued use of the website following any changes constitutes
|
||||||
|
your acceptance of the updated terms.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3>10. Governing Law</h3>
|
||||||
|
<p>
|
||||||
|
These Terms and Conditions are governed by and construed in accordance
|
||||||
|
with the laws of France and applicable EU regulations.
|
||||||
|
</p>
|
||||||
|
<h3>11. Contact Information</h3>
|
||||||
|
<p>
|
||||||
|
For questions or support, please contact:{" "}
|
||||||
|
<a href="https://dirnum.univ-amu.fr/fr">
|
||||||
|
Aix-Marseille University IT Services
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+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} />
|
||||||
|
|||||||
+4
-1
@@ -4,7 +4,10 @@ import { FreshContext } from "$fresh/server.ts";
|
|||||||
export default async function Home(_request: Request, _context: FreshContext) {
|
export default async function Home(_request: Request, _context: FreshContext) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Welcome to PolyMPR!</h2>
|
<h2>PolyMPR</h2>
|
||||||
|
<h3>
|
||||||
|
The <em>ultimate</em> HR platform
|
||||||
|
</h3>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-50
@@ -1,5 +1,5 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { FreshContext, 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 { parse, type RegularTagNode } from "@melvdouc/xml-parser";
|
||||||
import {
|
import {
|
||||||
CasContent,
|
CasContent,
|
||||||
@@ -11,16 +11,25 @@ import { createJwt } from "@popov/jwt";
|
|||||||
import { setCookie } from "$std/http/cookie.ts";
|
import { setCookie } from "$std/http/cookie.ts";
|
||||||
import { getKey } from "$root/routes/_middleware.ts";
|
import { getKey } from "$root/routes/_middleware.ts";
|
||||||
|
|
||||||
const SERVICE = "https://localhost/login";
|
|
||||||
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[]> = {};
|
||||||
@@ -29,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 {
|
||||||
@@ -38,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,
|
||||||
};
|
};
|
||||||
@@ -52,56 +58,58 @@ 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`;
|
||||||
|
|
||||||
if (ticket) {
|
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) {
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
Location: "/apps",
|
Location: context.state.isAuthenticated
|
||||||
},
|
? "/apps"
|
||||||
});
|
: `${CAS}/login?service=${service}`,
|
||||||
} else {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
|
||||||
headers: {
|
|
||||||
Location: `${CAS}/login?service=${SERVICE}`,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-7
@@ -1,18 +1,22 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { FreshContext, 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";
|
import { deleteCookie } from "$std/http/cookie.ts";
|
||||||
|
|
||||||
const SERVICE = "https://localhost/";
|
|
||||||
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();
|
||||||
|
|
||||||
deleteCookie(headers, "sessionToken", { path: "/" });
|
deleteCookie(headers, "sessionToken", { path: "/" });
|
||||||
headers.set("Location", `${CAS}/logout?service=${SERVICE}`);
|
headers.set("Location", `${CAS}/logout?service=${context.url.origin}`);
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
|
|||||||
+10
-91
@@ -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 { createModule } from "$root/toolbox/module/create.ts";
|
||||||
import { listModules } from "$root/toolbox/module/list.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<string>) => void;
|
/**
|
||||||
type CLIAsyncCommand = (...args: Array<string>) => Promise<void>;
|
* CLI will use `args._`, but you can define options for global CLI.
|
||||||
interface CLI {
|
*/
|
||||||
[command: string]: CLI | CLICommand | CLIAsyncCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
const argSpec: ParseOptions = {};
|
const argSpec: ParseOptions = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure CLI commands here.
|
||||||
|
*/
|
||||||
const cli: CLI = {
|
const cli: CLI = {
|
||||||
help: () => displayHelp(cli),
|
help: () => displayHelp(cli),
|
||||||
module: {
|
module: {
|
||||||
@@ -18,87 +20,4 @@ const cli: CLI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function displayHelp(
|
main(cli, argSpec);
|
||||||
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<string | number>,
|
|
||||||
cli: CLI,
|
|
||||||
): never | {
|
|
||||||
command: CLICommand | CLIAsyncCommand;
|
|
||||||
args: Array<string | number>;
|
|
||||||
} {
|
|
||||||
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();
|
|
||||||
|
|||||||
@@ -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<string | number>,
|
||||||
|
cli: CLI,
|
||||||
|
): never | {
|
||||||
|
command: CLICommand | CLIAsyncCommand;
|
||||||
|
args: Array<string | number>;
|
||||||
|
} {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
export type CLICommand = (...args: Array<string>) => void;
|
||||||
|
export type CLIAsyncCommand = (...args: Array<string>) => Promise<void>;
|
||||||
|
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(" ") ?? ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Creates a new module.
|
||||||
|
* @param name The module name.
|
||||||
|
*/
|
||||||
export async function createModule(name: string): Promise<void> {
|
export async function createModule(name: string): Promise<void> {
|
||||||
if (!name.match(/^[a-zA-Z0-9](?:(?:\-(?!\-))?[a-zA-Z0-9]*)*[a-zA-Z0-9]$/)) {
|
if (!name.match(/^[a-zA-Z0-9](?:(?:\-(?!\-))?[a-zA-Z0-9]*)*[a-zA-Z0-9]$/)) {
|
||||||
console.error("Module names must be in kebab case.");
|
console.error("Module names must be in kebab case.");
|
||||||
@@ -58,16 +62,32 @@ export async function createModule(name: string): Promise<void> {
|
|||||||
formatter.output();
|
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<void> {
|
function createFile(path: string, content: string): Promise<void> {
|
||||||
console.log(`Creating file ${path}...`);
|
console.log(`Creating file ${path}...`);
|
||||||
return Deno.writeTextFile(path, content);
|
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<void> {
|
function createDir(path: string): Promise<void> {
|
||||||
console.log(`Creating directory ${path}...`);
|
console.log(`Creating directory ${path}...`);
|
||||||
return Deno.mkdir(path);
|
return Deno.mkdir(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the index content.
|
||||||
|
* @param _name The module capitalized name.
|
||||||
|
* @returns The index content.
|
||||||
|
*/
|
||||||
function getIndexContent(_name: string) {
|
function getIndexContent(_name: string) {
|
||||||
return `
|
return `
|
||||||
import makeIndex from "$root/defaults/makeIndex.ts";
|
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) {
|
function getPartialIndexContent(name: string) {
|
||||||
return `
|
return `
|
||||||
import {
|
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) {
|
function getPropsContent(name: string) {
|
||||||
return `
|
return `
|
||||||
import { AppProperties } from "$root/defaults/interfaces.ts";
|
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) {
|
function getApiExampleContent(_name: string) {
|
||||||
return `
|
return `
|
||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* List all modules of PolyMPR.
|
||||||
|
*/
|
||||||
export async function listModules(): Promise<void> {
|
export async function listModules(): Promise<void> {
|
||||||
for await (const path of Deno.readDir("routes/(apps)")) {
|
for await (const path of Deno.readDir("routes/(apps)")) {
|
||||||
if (path.isDirectory) {
|
if (path.isDirectory) {
|
||||||
|
|||||||
Reference in New Issue
Block a user