diff --git a/express/app.ts b/express/app.ts index 592a1cd..2e4cc0b 100644 --- a/express/app.ts +++ b/express/app.ts @@ -15,6 +15,7 @@ import { AuthorizationDenied, type Call, type InternalHandler, + isRedirect, type Method, massageMethod, methodParser, @@ -25,8 +26,9 @@ import { const app = express(); -// Parse JSON request bodies +// Parse request bodies app.use(express.json()); +app.use(express.urlencoded({ extended: true })); services.logging.log({ source: "logging", text: ["1"] }); const processedRoutes: { [K in Method]: ProcessedRoute[] } = { @@ -111,8 +113,10 @@ async function handler( const method = await methodParser.parseAsync(req.method); const byMethod = processedRoutes[method]; + console.log("DEBUG: req.path =", JSON.stringify(req.path), "method =", method); for (const [_idx, pr] of byMethod.entries()) { - const match = pr.matcher(req.url); + const match = pr.matcher(req.path); + console.log("DEBUG: trying pattern, match result =", match); if (match) { console.log("match", match); const resp = await pr.handler(req); @@ -124,7 +128,7 @@ async function handler( const retval: Result = { code: httpCodes.clientErrors.NotFound, contentType: contentTypes.text.plain, - result: "not found", + result: "not found!", }; return retval; @@ -138,7 +142,18 @@ app.use(async (req: ExpressRequest, res: ExpressResponse) => { console.log(result); - res.status(code).send(result); + // Set any cookies from the result + if (result0.cookies) { + for (const cookie of result0.cookies) { + res.cookie(cookie.name, cookie.value, cookie.options ?? {}); + } + } + + if (isRedirect(result0)) { + res.redirect(code, result0.redirect); + } else { + res.status(code).send(result); + } }); process.title = `diachron:${cli.listen.port}`; diff --git a/express/basic/login.ts b/express/basic/login.ts new file mode 100644 index 0000000..abbaee9 --- /dev/null +++ b/express/basic/login.ts @@ -0,0 +1,62 @@ +import { SESSION_COOKIE_NAME } from "../auth/token"; +import { tokenLifetimes } from "../auth/types"; +import { services } from "../services"; +import type { Call, Result, Route } from "../types"; +import { html, redirect, render } from "../util"; + +const loginHandler = async (call: Call): Promise => { + if (call.method === "GET") { + const c = await render("basic/login", {}); + return html(c); + } + + // POST - handle login + const { email, password } = call.request.body; + + if (!email || !password) { + const c = await render("basic/login", { + error: "Email and password are required", + email, + }); + return html(c); + } + + const result = await services.auth.login(email, password, "cookie", { + userAgent: call.request.get("User-Agent"), + ipAddress: call.request.ip, + }); + + if (!result.success) { + const c = await render("basic/login", { + error: result.error, + email, + }); + return html(c); + } + + // Success - set cookie and redirect to home + const redirectResult = redirect("/"); + redirectResult.cookies = [ + { + name: SESSION_COOKIE_NAME, + value: result.token, + options: { + httpOnly: true, + secure: false, // Set to true in production with HTTPS + sameSite: "lax", + maxAge: tokenLifetimes.session, + path: "/", + }, + }, + ]; + + return redirectResult; +}; + +const loginRoute: Route = { + path: "/login", + methods: ["GET", "POST"], + handler: loginHandler, +}; + +export { loginRoute }; diff --git a/express/basic/routes.ts b/express/basic/routes.ts index 6df97b9..fc01d60 100644 --- a/express/basic/routes.ts +++ b/express/basic/routes.ts @@ -1,18 +1,29 @@ import { DateTime } from "ts-luxon"; import type { Call, Result, Route } from "../types"; import { html, render } from "../util"; +import { loginRoute } from "./login"; const routes: Record = { hello: { path: "/hello", methods: ["GET"], - handler: async (call: Call): Promise => { + handler: async (_call: Call): Promise => { const now = DateTime.now(); const c = await render("basic/hello", { now }); return html(c); }, }, + home: { + path:'/', + methods:['GET'], + handler:async(_call:Call): Promise => { + const c = await render('basic/home') + + return html(c) + } + }, + login: loginRoute, }; export { routes }; diff --git a/express/routes.ts b/express/routes.ts index 2dc9585..ba3688a 100644 --- a/express/routes.ts +++ b/express/routes.ts @@ -25,7 +25,9 @@ const okText = (result: string): Result => { const routes: Route[] = [ ...authRoutes, + basicRoutes.home, basicRoutes.hello, + basicRoutes.login, { path: "/slow", methods: ["GET"], diff --git a/express/types.ts b/express/types.ts index 528eabc..222db51 100644 --- a/express/types.ts +++ b/express/types.ts @@ -49,12 +49,35 @@ export type ProcessedRoute = { handler: InternalHandler; }; +export type CookieOptions = { + httpOnly?: boolean; + secure?: boolean; + sameSite?: "strict" | "lax" | "none"; + maxAge?: number; + path?: string; +}; + +export type Cookie = { + name: string; + value: string; + options?: CookieOptions; +}; + export type Result = { code: HttpCode; contentType: ContentType; result: string; + cookies?: Cookie[]; }; +export type RedirectResult = Result & { + redirect: string; +}; + +export function isRedirect(result: Result): result is RedirectResult { + return "redirect" in result; +} + export type Route = { path: string; methods: Method[]; diff --git a/express/util.ts b/express/util.ts index 3d141d8..78e9d9b 100644 --- a/express/util.ts +++ b/express/util.ts @@ -3,7 +3,7 @@ import nunjucks from "nunjucks"; import { contentTypes } from "./content-types"; import { executionContext } from "./execution-context"; import { httpCodes } from "./http-codes"; -import type { Result } from "./types"; +import type { Result, RedirectResult } from "./types"; // FIXME: Handle the error here const loadFile = async (path: string): Promise => { @@ -13,12 +13,14 @@ const loadFile = async (path: string): Promise => { return data; }; -const render = async (path: string, ctx: object): Promise => { +const render = async (path: string, ctx?: object): Promise => { const fullPath = `${executionContext.diachron_root}/templates/${path}.html.njk`; const template = await loadFile(fullPath); - const retval = nunjucks.renderString(template, ctx); + const c = ctx===undefined ? {} : ctx; + + const retval = nunjucks.renderString(template, c); return retval; }; @@ -33,4 +35,13 @@ const html = (payload: string): Result => { return retval; }; -export { render, html }; +const redirect = (location: string): RedirectResult => { + return { + code: httpCodes.redirection.SeeOther, + contentType: contentTypes.text.plain, + result: "", + redirect: location, + }; +}; + +export { render, html, redirect }; diff --git a/templates/basic/login.html.njk b/templates/basic/login.html.njk new file mode 100644 index 0000000..6b9ae4f --- /dev/null +++ b/templates/basic/login.html.njk @@ -0,0 +1,55 @@ + + + Login + + + +

Login

+ {% if error %} +
{{ error }}
+ {% endif %} +
+ + + +
+ +