Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/features/add-todo/addToDoPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@ import { describe, expect, test } from "bun:test";
import { createAppConfig } from "@shared/appConfig.ts";
import { createAppConfigMiddleware } from "@shared/appConfigMiddleware.ts";
import type { AppVariables } from "@shared/appVariables.ts";
import {
createTodoJwtPayload,
type JwtPayload,
} from "@shared/pageJwtMiddleware.ts";
import { pageRoutes } from "@shared/pageRoutes.ts";
import { Hono } from "hono";
import { sign } from "hono/jwt";
import { addToDoPage } from "./addToDoPage.tsx";

const fixedPayloadDate = new Date("2024-01-01T00:00:00.000Z");
const fixedIatInSeconds = Math.floor(fixedPayloadDate.getTime() / 1000);
const fixedExpInSeconds = Math.floor(
new Date("2100-01-01T00:00:00.000Z").getTime() / 1000,
);

const createJwtPayload = (): JwtPayload => {
return {
...createTodoJwtPayload("1234", "admin", "admin", fixedPayloadDate),
exp: fixedExpInSeconds,
iat: fixedIatInSeconds,
};
};

describe("addToDoPage", () => {
test("redirects to login when JWT cookie is missing", async () => {
const appConfig = createAppConfig({
Expand All @@ -28,11 +46,7 @@ describe("addToDoPage", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign(
{ sub: "1234", preferred_username: "admin" },
appConfig.jwt.secret,
"HS256",
);
const token = await sign(createJwtPayload(), appConfig.jwt.secret, "HS256");
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.route(pageRoutes.ADD_TODO, addToDoPage);
Expand Down
4 changes: 1 addition & 3 deletions src/features/add-todo/addToDoPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import type { AppVariables } from "@shared/appVariables.ts";
import { Page } from "@shared/page.tsx";
import { pageJwtMiddleware } from "@shared/pageJwtMiddleware.ts";
import { getUsernameFromJwtPayload } from "@shared/utility.ts";
import { Hono } from "hono";

export const addToDoPage = new Hono<{ Variables: AppVariables }>()
.use("/", pageJwtMiddleware)
.get("/", (c) => {
const username = getUsernameFromJwtPayload(c.var.jwtPayload);

const username = c.var.validJwtPayload.preferred_username;
return c.html(
<Page type="authenticated" currentPath={c.req.path} username={username}>
<h1>Add ToDo</h1>
Expand Down
24 changes: 19 additions & 5 deletions src/features/home/homePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@ import { describe, expect, test } from "bun:test";
import { createAppConfig } from "@shared/appConfig.ts";
import { createAppConfigMiddleware } from "@shared/appConfigMiddleware.ts";
import type { AppVariables } from "@shared/appVariables.ts";
import {
createTodoJwtPayload,
type JwtPayload,
} from "@shared/pageJwtMiddleware.ts";
import { pageRoutes } from "@shared/pageRoutes.ts";
import { Hono } from "hono";
import { sign } from "hono/jwt";
import { homePage } from "./homePage.tsx";

const fixedPayloadDate = new Date("2024-01-01T00:00:00.000Z");
const fixedIatInSeconds = Math.floor(fixedPayloadDate.getTime() / 1000);
const fixedExpInSeconds = Math.floor(
new Date("2100-01-01T00:00:00.000Z").getTime() / 1000,
);

const createJwtPayload = (): JwtPayload => {
return {
...createTodoJwtPayload("1234", "admin", "admin", fixedPayloadDate),
exp: fixedExpInSeconds,
iat: fixedIatInSeconds,
};
};

describe("homePage", () => {
test("redirects to login when JWT cookie is missing", async () => {
const appConfig = createAppConfig({
Expand All @@ -26,11 +44,7 @@ describe("homePage", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign(
{ sub: "1234", preferred_username: "admin" },
appConfig.jwt.secret,
"HS256",
);
const token = await sign(createJwtPayload(), appConfig.jwt.secret, "HS256");
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.route("/", homePage);
Expand Down
3 changes: 1 addition & 2 deletions src/features/home/homePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { AppVariables } from "@shared/appVariables.ts";
import { Page } from "@shared/page.tsx";
import { pageJwtMiddleware } from "@shared/pageJwtMiddleware.ts";
import { getUsernameFromJwtPayload } from "@shared/utility.ts";
import { Hono } from "hono";

export const homePage = new Hono<{ Variables: AppVariables }>()
.use("/", pageJwtMiddleware)
.get("/", (c) => {
const username = getUsernameFromJwtPayload(c.var.jwtPayload);
const username = c.var.validJwtPayload.preferred_username;
return c.html(
<Page type="authenticated" currentPath={c.req.path} username={username}>
<h1>Home</h1>
Expand Down
18 changes: 7 additions & 11 deletions src/features/login/loginApi.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { zValidator } from "@hono/zod-validator";
import type { AppVariables } from "@shared/appVariables.ts";
import { sseRedirect } from "@shared/datastar.ts";
import { createTodoJwtPayload } from "@shared/pageJwtMiddleware.ts";
import { pageRoutes } from "@shared/pageRoutes.ts";
import { getUserByUsername } from "@shared/user.ts";
import { addDays } from "date-fns";
Expand Down Expand Up @@ -37,18 +38,13 @@ export const loginApi = new Hono<{ Variables: AppVariables }>().post(
if (!isPasswordValid) {
throw new HTTPException(401, { message: "Invalid credentials" });
}
const token = await sign(
{
sub: user.id,
preferred_username: user.username,
role: "admin",
iss: "todo-app",
exp: Math.floor(addDays(now, 1).getTime() / 1000),
iat: Math.floor(now.getTime() / 1000),
},
appConfig.jwt.secret,
"HS256",
const jwtPayload = createTodoJwtPayload(
user.id,
user.username,
"admin",
now,
);
const token = await sign(jwtPayload, appConfig.jwt.secret, "HS256");
setCookie(c, appConfig.jwt.cookieName, token, {
httpOnly: true,
sameSite: "Strict",
Expand Down
6 changes: 4 additions & 2 deletions src/shared/appVariables.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { JwtVariables } from "hono/jwt";
import type { Logger } from "winston";
import type { AppConfig } from "./appConfig.ts";
import type { Clock } from "./clock.ts";
import type { JwtPayload } from "./pageJwtMiddleware.ts";

export type AppVariables = {
clock: Clock;
appConfig: AppConfig;
logger: Logger;
} & JwtVariables;
jwtPayload: unknown;
validJwtPayload: JwtPayload;
};
67 changes: 63 additions & 4 deletions src/shared/pageJwtMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { describe, expect, test } from "bun:test";
import type { AppVariables } from "@shared/appVariables.ts";
import { addDays } from "date-fns";
import { Hono } from "hono";
import { sign } from "hono/jwt";
import { createAppConfig } from "./appConfig.ts";
import { createAppConfigMiddleware } from "./appConfigMiddleware.ts";
import { pageJwtMiddleware } from "./pageJwtMiddleware.ts";
import type { AppVariables } from "./appVariables.ts";
import {
createTodoJwtPayload,
type JwtPayload,
pageJwtMiddleware,
} from "./pageJwtMiddleware.ts";
import { pageRoutes } from "./pageRoutes.ts";

const fixedPayloadDate = new Date("2024-01-01T00:00:00.000Z");
const fixedIatInSeconds = Math.floor(fixedPayloadDate.getTime() / 1000);
const fixedExpInSeconds = Math.floor(
new Date("2100-01-01T00:00:00.000Z").getTime() / 1000,
);

const createJwtPayload = (overrides: Partial<JwtPayload> = {}): JwtPayload => {
return {
...createTodoJwtPayload("1234", "admin", "admin", fixedPayloadDate),
exp: fixedExpInSeconds,
iat: fixedIatInSeconds,
...overrides,
};
};

describe("pageJwtMiddleware", () => {
test("redirects to login when JWT cookie is missing", async () => {
const appConfig = createAppConfig({
Expand Down Expand Up @@ -48,7 +68,7 @@ describe("pageJwtMiddleware", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign({ sub: "1234" }, appConfig.jwt.secret, "HS256");
const token = await sign(createJwtPayload(), appConfig.jwt.secret, "HS256");
let handlerReached = false;
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
Comment on lines 68 to 74
Expand All @@ -74,7 +94,7 @@ describe("pageJwtMiddleware", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign({ sub: "1234" }, appConfig.jwt.secret, "HS256");
const token = await sign(createJwtPayload(), appConfig.jwt.secret, "HS256");
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.use("*", pageJwtMiddleware)
Expand All @@ -93,3 +113,42 @@ describe("pageJwtMiddleware", () => {
expect(response.status).toBe(500);
});
});

describe("createTodoJwtPayload", () => {
test("maps user fields and static issuer", () => {
const now = new Date("2026-05-24T12:00:00.000Z");

const payload = createTodoJwtPayload("user-123", "admin", "editor", now);

expect(payload.sub).toBe("user-123");
expect(payload.preferred_username).toBe("admin");
expect(payload.role).toBe("editor");
expect(payload.iss).toBe("todo-app");
});

test("sets iat to current time in seconds", () => {
const now = new Date("2026-05-24T12:34:56.000Z");

const payload = createTodoJwtPayload("user-1", "user", "admin", now);

expect(payload.iat).toBe(Math.floor(now.getTime() / 1000));
});

test("sets exp to one day after current time in seconds", () => {
const now = new Date("2026-05-24T12:34:56.000Z");

const payload = createTodoJwtPayload("user-1", "user", "admin", now);

expect(payload.exp).toBe(Math.floor(addDays(now, 1).getTime() / 1000));
expect(payload.exp - payload.iat).toBe(24 * 60 * 60);
});

test("floors fractional milliseconds for both iat and exp", () => {
const now = new Date("2026-05-24T12:34:56.789Z");

const payload = createTodoJwtPayload("user-1", "user", "admin", now);

expect(payload.iat).toBe(Math.floor(now.getTime() / 1000));
expect(payload.exp).toBe(Math.floor(addDays(now, 1).getTime() / 1000));
});
});
32 changes: 32 additions & 0 deletions src/shared/pageJwtMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,38 @@
import { addDays } from "date-fns";
import { createMiddleware } from "hono/factory";
import { jwt } from "hono/jwt";
import z from "zod";
import type { AppVariables } from "./appVariables.ts";
import { pageRoutes } from "./pageRoutes.ts";

const jwtPayloadSchema = z.object({
sub: z.string(),
preferred_username: z.string(),
role: z.string(),
iss: z.string(),
exp: z.number(),
Comment on lines +8 to +13
iat: z.number(),
});

export type JwtPayload = z.infer<typeof jwtPayloadSchema>;

export function createTodoJwtPayload(
userId: string,
username: string,
role: string,
now: Date,
): JwtPayload {
const iat: number = Math.floor(now.getTime() / 1000);
return {
sub: userId,
preferred_username: username,
role,
iss: "todo-app",
exp: Math.floor(addDays(now, 1).getTime() / 1000),
iat,
};
}

export const pageJwtMiddleware = createMiddleware<{ Variables: AppVariables }>(
async (c, next) => {
const appConfig = c.var.appConfig;
Expand All @@ -14,6 +44,8 @@ export const pageJwtMiddleware = createMiddleware<{ Variables: AppVariables }>(

try {
await verifyJwt(c, async () => {});
const jwtPayload = jwtPayloadSchema.parse(c.var.jwtPayload);
c.set("validJwtPayload", jwtPayload);
} catch {
return c.redirect(pageRoutes.LOGIN);
}
Expand Down
38 changes: 0 additions & 38 deletions src/shared/utility.test.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/shared/utility.ts

This file was deleted.