Add Session class to provide getUser() on call.session

Wraps SessionData and user into a Session class that handlers can use
via call.session.getUser() instead of accessing services directly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-04 15:22:27 -06:00
parent ad6d405206
commit 74d75d08dd
5 changed files with 64 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ import express, {
type Response as ExpressResponse,
} from "express";
import { match } from "path-to-regexp";
import { Session } from "./auth";
import { cli } from "./cli";
import { contentTypes } from "./content-types";
import { httpCodes } from "./http-codes";
@@ -68,7 +69,7 @@ routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
parameters: { one: 1, two: 2 },
request,
user: auth.user,
session: auth.session,
session: new Session(auth.session, auth.user),
};
try {

View File

@@ -10,4 +10,11 @@ export { hashPassword, verifyPassword } from "./password";
export { AuthService, type AuthResult } from "./service";
export { type AuthStore, InMemoryAuthStore } from "./store";
export { generateToken, hashToken, SESSION_COOKIE_NAME } from "./token";
export * from "./types";
export {
type AuthMethod,
type SessionData,
type TokenId,
type TokenType,
Session,
tokenLifetimes,
} from "./types";

View File

@@ -62,3 +62,35 @@ export const tokenLifetimes: Record<TokenType, number> = {
password_reset: 1 * 60 * 60 * 1000, // 1 hour
email_verify: 24 * 60 * 60 * 1000, // 24 hours
};
// Import here to avoid circular dependency at module load time
import { AnonymousUser, type MaybeUser } from "../user";
// Session wrapper class providing a consistent interface for handlers.
// Always present on Call (never null), but may represent an anonymous session.
export class Session {
constructor(
private readonly data: SessionData | null,
private readonly user: MaybeUser,
) {}
getUser(): MaybeUser {
return this.user;
}
getData(): SessionData | null {
return this.data;
}
isAuthenticated(): boolean {
return this.user !== AnonymousUser;
}
get tokenId(): string | undefined {
return this.data?.tokenId;
}
get userId(): string | undefined {
return this.data?.userId;
}
}

View File

@@ -51,19 +51,35 @@ const routes: Route[] = [
return ret;
};
const rrr = lr(routes)
const template=`
<html>
<head></head>
<body>
<ul>
{% for route in rrr %}
<li><a href="{{ route }}">{{ route }}</a></li>
{% endfor %}
</ul>
</body>
</html>
`
const result = nunjucks.renderString(template,{rrr})
const listing = lr(routes).join(", ");
return {
code,
result: listing + "\n",
contentType: contentTypes.text.plain,
result,
contentType: contentTypes.text.html,
};
},
},
{
path: "/whoami",
methods: ["GET"],
handler: async (_call: Call): Promise<Result> => {
const me = services.session.getUser();
handler: async (call: Call): Promise<Result> => {
const me = call.session.getUser();
const template = `
<html>
<head></head>

View File

@@ -8,7 +8,7 @@ import {
} from "express";
import type { MatchFunction } from "path-to-regexp";
import { z } from "zod";
import type { SessionData } from "./auth/types";
import type { Session } from "./auth/types";
import { type ContentType, contentTypes } from "./content-types";
import { type HttpCode, httpCodes } from "./http-codes";
import {
@@ -40,7 +40,7 @@ export type Call = {
parameters: object;
request: ExpressRequest;
user: MaybeUser;
session: SessionData | null;
session: Session;
};
export type InternalHandler = (req: ExpressRequest) => Promise<Result>;