Adds /login route with HTML template that handles GET (show form) and POST (authenticate). On successful login, sets session cookie and redirects to /. Also adds framework support for redirects and cookies in route handlers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
121 lines
2.8 KiB
TypeScript
121 lines
2.8 KiB
TypeScript
// types.ts
|
|
|
|
// FIXME: split this up into types used by app developers and types internal
|
|
// to the framework.
|
|
import type { Request as ExpressRequest } from "express";
|
|
import type { MatchFunction } from "path-to-regexp";
|
|
import { z } from "zod";
|
|
import type { Session } from "./auth/types";
|
|
import type { ContentType } from "./content-types";
|
|
import type { HttpCode } from "./http-codes";
|
|
import {
|
|
AnonymousUser,
|
|
type MaybeUser,
|
|
type Permission,
|
|
type User,
|
|
} from "./user";
|
|
|
|
const methodParser = z.union([
|
|
z.literal("GET"),
|
|
z.literal("POST"),
|
|
z.literal("PUT"),
|
|
z.literal("PATCH"),
|
|
z.literal("DELETE"),
|
|
]);
|
|
|
|
export type Method = z.infer<typeof methodParser>;
|
|
const massageMethod = (input: string): Method => {
|
|
const r = methodParser.parse(input.toUpperCase());
|
|
|
|
return r;
|
|
};
|
|
|
|
export type Call = {
|
|
pattern: string;
|
|
path: string;
|
|
method: Method;
|
|
parameters: object;
|
|
request: ExpressRequest;
|
|
user: MaybeUser;
|
|
session: Session;
|
|
};
|
|
|
|
export type InternalHandler = (req: ExpressRequest) => Promise<Result>;
|
|
|
|
export type Handler = (call: Call) => Promise<Result>;
|
|
export type ProcessedRoute = {
|
|
matcher: MatchFunction<Record<string, string>>;
|
|
method: Method;
|
|
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[];
|
|
handler: Handler;
|
|
interruptable?: boolean;
|
|
};
|
|
|
|
// Authentication error classes
|
|
export class AuthenticationRequired extends Error {
|
|
constructor() {
|
|
super("Authentication required");
|
|
this.name = "AuthenticationRequired";
|
|
}
|
|
}
|
|
|
|
export class AuthorizationDenied extends Error {
|
|
constructor() {
|
|
super("Authorization denied");
|
|
this.name = "AuthorizationDenied";
|
|
}
|
|
}
|
|
|
|
// Helper for handlers to require authentication
|
|
export function requireAuth(call: Call): User {
|
|
if (call.user === AnonymousUser) {
|
|
throw new AuthenticationRequired();
|
|
}
|
|
return call.user;
|
|
}
|
|
|
|
// Helper for handlers to require specific permission
|
|
export function requirePermission(call: Call, permission: Permission): User {
|
|
const user = requireAuth(call);
|
|
if (!user.hasPermission(permission)) {
|
|
throw new AuthorizationDenied();
|
|
}
|
|
return user;
|
|
}
|
|
|
|
export { methodParser, massageMethod };
|