Skip to content

Playground

Switch one on and see what it does

Nothing is graded here and the defect is named before you start. This is where to come when you want to understand a class rather than be tested on one.

Handlers

1
2/** The password every fixture account uses. Fixtures, not secrets. */
3const FIXTURE_PASSWORD = "correct horse";
4
5export function signIn(req: Req, ctx: Ctx) {
6 const email = String(req.body.email ?? "");
7 const password = String(req.body.password ?? "");
8 if (email === "" || password === "") {
9 return ctx.res.status(400).json({ error: "email and password are required" });
10 }
11
12 const user = ctx.db.queryOne("SELECT id, role, name FROM users WHERE email = ?", [email]);
13 // The same answer either way, so a caller cannot tell an unknown address
14 // from a wrong password.
15 if (!user || password !== FIXTURE_PASSWORD) {
16 return ctx.res.status(401).json({ error: "email or password is wrong" });
17 }
18
19 // A new session id at the privilege change, so an id planted beforehand is
20 // not the one that ends up authenticated.
21 ctx.session.rotate(String(user.id), [String(user.role)]);
22 ctx.session.attach(ctx.res, { httpOnly: true, secure: true, sameSite: "Lax", path: "/" });
23 ctx.log.info("sign-in", { userId: user.id });
24 return ctx.res.json({ id: user.id, name: user.name, role: user.role });
25}
26
27export function sessionToken(req: Req, ctx: Ctx) {
28 const session = ctx.session.current();
29 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
30 return ctx.res.json({ token: ctx.session.csrfToken() });
31}
32
33export function signOut(req: Req, ctx: Ctx) {
34 ctx.session.destroy();
35 return ctx.res.status(204).text("");
36}
37
38export function listOrders(req: Req, ctx: Ctx) {
39 const session = ctx.session.current();
40 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
41
42 const status = req.query.status;
43 const rows = status
44 ? ctx.db.query(
45 "SELECT id, total, status, placed_at FROM orders WHERE user_id = ? AND status = ? ORDER BY placed_at DESC",
46 [session.subject, status],
47 )
48 : ctx.db.query("SELECT id, total, status, placed_at FROM orders WHERE user_id = ? ORDER BY placed_at DESC", [
49 session.subject,
50 ]);
51
52 return ctx.res.json({ orders: rows });
53}
54
55export function searchOrders(req: Req, ctx: Ctx) {
56 const session = ctx.session.current();
57 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
58
59 const term = String(req.query.q || "");
60 const rows = term
61 ? ctx.db.query("SELECT id, total, status FROM orders WHERE user_id = ? AND status LIKE ?", [
62 session.subject,
63 "%" + term + "%",
64 ])
65 : [];
66
67 return ctx.res.html(ctx.render("search", { term: term, count: rows.length }));
68}
69
70export function getOrder(req: Req, ctx: Ctx) {
71 const session = ctx.session.current();
72 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
73
74 const order = ctx.db.queryOne("SELECT id, user_id, total, status, placed_at FROM orders WHERE id = ?", [
75 req.params.id,
76 ]);
77 if (!order) return ctx.res.status(404).json({ error: "no such order" });
78
79 // Who it belongs to, not just who is asking.
80 if (String(order.user_id) !== session.subject) {
81 return ctx.res.status(404).json({ error: "no such order" });
82 }
83 ctx.touched({ table: "orders", id: String(order.id), ownerId: String(order.user_id), kind: "read" });
84
85 return ctx.res.json({ order: { id: order.id, total: order.total, status: order.status, placedAt: order.placed_at } });
86}
87
88export function orderPage(req: Req, ctx: Ctx) {
89 const session = ctx.session.current();
90 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
91
92 const order = ctx.db.queryOne("SELECT id, user_id, total, status FROM orders WHERE id = ?", [req.params.id]);
93 if (!order || String(order.user_id) !== session.subject) {
94 return ctx.res.status(404).json({ error: "no such order" });
95 }
96 ctx.touched({ table: "orders", id: String(order.id), ownerId: String(order.user_id), kind: "read" });
97
98 const note = ctx.db.queryOne("SELECT body FROM notes WHERE order_id = ?", [String(order.id)]);
99 return ctx.res.html(ctx.render("order", { order: order, note: note ? note.body : "" }));
100}
101
102export function updateOrder(req: Req, ctx: Ctx) {
103 const session = ctx.session.current();
104 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
105
106 // The cookie says who you are. It does not say that you meant to send this.
107 if (!ctx.session.checkCsrf(req.headers["x-csrf-token"])) {
108 return ctx.res.status(403).json({ error: "bad or missing token" });
109 }
110
111 const order = ctx.db.queryOne("SELECT id, user_id FROM orders WHERE id = ?", [req.params.id]);
112 if (!order || String(order.user_id) !== session.subject) {
113 return ctx.res.status(404).json({ error: "no such order" });
114 }
115
116 // Only the field this route declares. A body carrying anything else is
117 // ignored rather than trusted.
118 const status = req.body.status;
119 if (typeof status !== "string" || status === "") {
120 return ctx.res.status(400).json({ error: "status is required" });
121 }
122
123 ctx.db.execute("UPDATE orders SET status = ? WHERE id = ?", [status, String(order.id)]);
124 ctx.touched({
125 table: "orders",
126 id: String(order.id),
127 ownerId: String(order.user_id),
128 kind: "write",
129 fields: ["status"],
130 writable: ["status"],
131 });
132 return ctx.res.json({ id: order.id, status: status });
133}
134
135export function listUsers(req: Req, ctx: Ctx) {
136 const session = ctx.session.current();
137 if (!session || !session.subject) return ctx.res.status(401).json({ error: "sign in" });
138
139 // Who you are is not whether you may.
140 if (session.roles.indexOf("admin") === -1) {
141 return ctx.res.status(403).json({ error: "not allowed" });
142 }
143
144 const rows = ctx.db.query("SELECT id, email, role, name FROM users ORDER BY id");
145 return ctx.res.json({ users: rows });
146}
147
148/* The shapes the framework hands a handler. Declarations, not behaviour. */
149
150interface Ctx {
151 db: {
152 query(sql: string, params?: unknown[]): Record<string, unknown>[];
153 queryOne(sql: string, params?: unknown[]): Record<string, unknown> | undefined;
154 execute(sql: string, params?: unknown[]): Record<string, unknown>[];
155 };
156 session: {
157 current(): { id: string; subject: string | null; roles: string[] } | undefined;
158 rotate(subject: string, roles: string[]): { id: string };
159 authenticate(subject: string, roles: string[]): { id: string };
160 destroy(): void;
161 attach(res: unknown, options?: Record<string, unknown>): void;
162 csrfToken(): string;
163 checkCsrf(token: string | undefined): boolean;
164 };
165 res: {
166 status(code: number): Ctx["res"];
167 json(value: unknown): unknown;
168 html(value: string): unknown;
169 text(value: string): unknown;
170 };
171 log: { info(message: string, fields?: Record<string, unknown>): void };
172 render(template: string, values: Record<string, unknown>): string;
173 touched(access: Record<string, unknown>): void;
174 now(): number;
175}
176
177interface Req {
178 method: string;
179 path: string;
180 query: Record<string, string>;
181 params: Record<string, string>;
182 body: Record<string, unknown>;
183 headers: Record<string, string>;
184}
185

Console

shop.example.com

Nothing sent yet. Every request goes to the application in this tab and nowhere else.

Cookies

Empty. Anything the application sets lands here, and you can edit it.

Application

Defect