Project Setup
Starting point
This course builds on the capstone from “Authentication with Hectoday HTTP.” If you completed that course, you have a project with signup, login, logout, sessions, JWTs, and role-based access control.
If you did not complete it, you can still follow along. The project structure is straightforward and we will show the relevant code as we modify it.
Copy or fork the auth course project
Start with the auth course capstone files:
src/
app.ts # setup() with routes, hooks
server.ts # starts the server
db.ts # User type, in-memory store, seed data
schemas.ts # Zod schemas
sessions.ts # session store
cookies.ts # cookie helpers
jwt.ts # JWT helpers
auth.ts # authenticate, requireAdmin, authenticatedAdmin
routes/
auth.ts # POST /signup, POST /login, POST /logout
users.ts # GET /me, GET /users, DELETE /users/:id
token-auth.ts # POST /token/login, GET /token/me If you are starting fresh, install the dependencies:
npm install @hectoday/http zod srvx bcryptjs jose
npm install -D typescript @types/node @types/bcryptjs tsx Add session expiry
The auth course’s getSession stored sessions without expiry. Let’s add it now. Update src/sessions.ts:
// src/sessions.ts
export interface Session {
userId: string;
createdAt: number;
}
const store = new Map<string, Session>();
const MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
export function createSession(userId: string): string {
const id = crypto.randomUUID();
store.set(id, { userId, createdAt: Date.now() });
return id;
}
export function getSession(id: string): Session | undefined {
const session = store.get(id);
if (!session) return undefined;
if (Date.now() - session.createdAt > MAX_AGE) {
store.delete(id);
return undefined;
}
return session;
}
export function deleteSession(id: string): void {
store.delete(id);
}
export function deleteUserSessions(userId: string): void {
for (const [id, session] of store) {
if (session.userId === userId) {
store.delete(id);
}
}
} Two changes from the auth course version: expired sessions are rejected and cleaned up automatically, and deleteUserSessions lets us invalidate all sessions for a user (useful after a password change or account compromise).
Add structured logging
Throughout this course, we will log security events: failed logins, rate limit hits, account lockouts, password resets. A simple console.log works but structured logging makes events searchable and filterable.
Create src/logger.ts:
// src/logger.ts
export function log(event: string, data: Record<string, unknown> = {}): void {
console.log(
JSON.stringify({
timestamp: new Date().toISOString(),
event,
...data,
}),
);
} Usage:
log("login_failed", { email: "[email protected]", reason: "wrong_password", ip: "1.2.3.4" });
// {"timestamp":"2025-01-01T00:00:00.000Z","event":"login_failed","email":"[email protected]","reason":"wrong_password","ip":"1.2.3.4"} JSON-formatted logs can be parsed by log aggregation tools (Datadog, Grafana, CloudWatch). We will use this log function throughout the course.
Add an IP extraction helper
Several defenses (rate limiting, logging) need the client’s IP address. Create src/ip.ts:
// src/ip.ts
export function getClientIp(request: Request): string {
// Behind a reverse proxy (most production setups)
const forwarded = request.headers.get("x-forwarded-for");
if (forwarded) {
return forwarded.split(",")[0].trim();
}
// Direct connection (development)
return "unknown";
} [!WARNING]
X-Forwarded-Foris set by reverse proxies (Nginx, Cloudflare, load balancers). In production, configure your proxy to set this header and trust only the first value. Without a proxy, this header can be spoofed by the client. For development on localhost, “unknown” is fine.
Update the app hooks
Add request logging to src/app.ts:
import { log } from "./logger.js";
import { getClientIp } from "./ip.js";
export const app = setup({
onRequest: ({ request }) => ({
startTime: Date.now(),
ip: getClientIp(request),
}),
routes: [...],
onResponse: ({ request, response, locals }) => {
const duration = Date.now() - locals.startTime;
const url = new URL(request.url);
log("request", {
method: request.method,
path: url.pathname,
status: response.status,
duration,
ip: locals.ip,
});
return response;
},
onError: ({ error, locals }) => {
log("unhandled_error", { error: String(error), ip: locals.ip });
return Response.json({ error: "Internal error" }, { status: 500 });
},
}); Every request now produces a structured log line. Security events (which we add in later lessons) will stand out in the log stream.
Verify it works
npm run dev Make a few requests and check the server output. You should see JSON log lines with timestamps, methods, paths, and status codes.
File layout
src/
app.ts
server.ts
db.ts
schemas.ts
sessions.ts # now with expiry and deleteUserSessions
cookies.ts
jwt.ts
auth.ts
logger.ts # new — structured logging
ip.ts # new — IP extraction
routes/
auth.ts
users.ts
token-auth.ts This is our baseline. Every lesson from here adds a defense.
Why do we add a deleteUserSessions function?
Why do we use JSON-formatted log output instead of plain text?