Skip to content

Commit 626cce5

Browse files
committed
Cleanup api tokens storage helper
1 parent 42f618d commit 626cce5

File tree

5 files changed

+121
-119
lines changed

5 files changed

+121
-119
lines changed

backend/src/http/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { HTTP_PORT } from "#environment.ts";
66
import type { BackendHTTPContext } from "#http/http.ts";
77
import api from "#http/route/api/api.ts";
88
import frontend from "#http/route/frontend.ts";
9-
import { deleteExpiredTokens } from "#http/storage/api/tokens.ts";
9+
import { tokensTable } from "#http/storage/api/tokens.ts";
1010
import { serve } from "@hono/node-server";
1111
import { Hono } from "hono";
1212
import { HTTPException } from "hono/http-exception";
@@ -59,7 +59,7 @@ async function beginDeleteTokenLoop(): Promise<void> {
5959
try {
6060
logger.debug?.("Deleting expired tokens");
6161

62-
const deletedCount = await deleteExpiredTokens(ctx.db);
62+
const deletedCount = await tokensTable.removeExpiredTokens(ctx.db);
6363

6464
logger.debug?.(`Deleted ${deletedCount} tokens`);
6565
} finally {

backend/src/http/middleware/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { validateToken } from "#http/storage/api/tokens.ts";
1+
import { tokensTable } from "#http/storage/api/tokens.ts";
22
import { createMiddleware } from "hono/factory";
33
import { HTTPException } from "hono/http-exception";
44
import type { Pool } from "pg";
@@ -18,7 +18,7 @@ export function authMiddleware(db: Pool) {
1818
});
1919
}
2020

21-
const user = await validateToken(db, authorization);
21+
const user = await tokensTable.validate(db, authorization);
2222

2323
if (user === null) {
2424
throw new HTTPException(401, {

backend/src/http/route/api/v1/auth/logIn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } from "#environment.ts";
22
import type { BackendHTTPContext } from "#http/http.ts";
33
import { validate } from "#http/middleware/zod.ts";
4-
import { generateToken } from "#http/storage/api/tokens.ts";
4+
import { tokensTable } from "#http/storage/api/tokens.ts";
55
import { Hono } from "hono";
66
import { HTTPException } from "hono/http-exception";
77
import { z } from "zod";
@@ -97,7 +97,7 @@ export default (backendCtx: BackendHTTPContext): Hono => {
9797
}),
9898
});
9999

100-
const [token, expiresAt] = await generateToken(
100+
const [token, expiresAt] = await tokensTable.generateAndInsert(
101101
backendCtx.db,
102102
userJSON.id,
103103
);

backend/src/http/route/api/v1/auth/logOut.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BackendHTTPContext } from "#http/http.ts";
2-
import { deleteToken } from "#http/storage/api/tokens.ts";
2+
import { tokensTable } from "#http/storage/api/tokens.ts";
33
import { Hono } from "hono";
44
import { HTTPException } from "hono/http-exception";
55

@@ -15,7 +15,7 @@ export default (backendCtx: BackendHTTPContext): Hono => {
1515
});
1616
}
1717

18-
if (!(await deleteToken(backendCtx.db, authorization))) {
18+
if (!(await tokensTable.remove(backendCtx.db, authorization))) {
1919
throw new HTTPException(401, {
2020
message: "Invalid or expired token",
2121
});

backend/src/http/storage/api/tokens.ts

Lines changed: 113 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -13,144 +13,146 @@ const TokenInfo = z.strictObject({
1313
expiresAt: z.date(),
1414
});
1515

16-
export async function generateToken(
17-
db: Pool,
18-
userID: string,
19-
): Promise<[token: string, expiry: Date]> {
20-
const secret = crypto.randomBytes(16);
21-
const expiresAt = new Date(Date.now() + TOKEN_LIFETIME);
22-
23-
const hash = Buffer.from(await crypto.subtle.digest(ALGORITHM, secret));
24-
25-
await db.query(
26-
`
27-
INSERT INTO "api_tokens" (
28-
"userID",
29-
"hash",
30-
"expiresAt"
31-
)
32-
VALUES ($1, $2, $3)
33-
`,
34-
[userID, hash, expiresAt],
35-
);
36-
37-
return [
38-
BigInt(userID).toString(16) + "." + secret.toString("hex"),
39-
expiresAt,
40-
];
41-
}
16+
export namespace tokensTable {
17+
export async function generateAndInsert(
18+
db: Pool,
19+
userID: string,
20+
): Promise<[token: string, expiry: Date]> {
21+
const secret = crypto.randomBytes(16);
22+
const expiresAt = new Date(Date.now() + TOKEN_LIFETIME);
23+
24+
const hash = Buffer.from(await crypto.subtle.digest(ALGORITHM, secret));
4225

43-
async function tokenKey(token: string): Promise<[bigint, Buffer] | null> {
44-
const splitIndex = token.indexOf(".");
26+
await db.query(
27+
`
28+
INSERT INTO "api_tokens" (
29+
"userID",
30+
"hash",
31+
"expiresAt"
32+
)
33+
VALUES ($1, $2, $3)
34+
`,
35+
[userID, hash, expiresAt],
36+
);
4537

46-
if (splitIndex === -1) {
47-
return null;
38+
return [
39+
BigInt(userID).toString(16) + "." + secret.toString("hex"),
40+
expiresAt,
41+
];
4842
}
4943

50-
const userIDPart = token.slice(0, splitIndex);
51-
const secretPart = token.slice(splitIndex + 1);
44+
async function tokenKey(token: string): Promise<[bigint, Buffer] | null> {
45+
const splitIndex = token.indexOf(".");
5246

53-
if (userIDPart.length === 0 || secretPart.length === 0) {
54-
return null;
55-
}
47+
if (splitIndex === -1) {
48+
return null;
49+
}
50+
51+
const userIDPart = token.slice(0, splitIndex);
52+
const secretPart = token.slice(splitIndex + 1);
5653

57-
try {
58-
var userID = BigInt("0x" + userIDPart);
59-
} catch (error) {
60-
if (!(error instanceof SyntaxError)) {
61-
throw error;
54+
if (userIDPart.length === 0 || secretPart.length === 0) {
55+
return null;
6256
}
6357

64-
return null;
65-
}
58+
try {
59+
var userID = BigInt("0x" + userIDPart);
60+
} catch (error) {
61+
if (!(error instanceof SyntaxError)) {
62+
throw error;
63+
}
6664

67-
const secretBuffer = Buffer.from(secretPart, "hex");
68-
const hash = Buffer.from(
69-
await crypto.subtle.digest(ALGORITHM, secretBuffer),
70-
);
65+
return null;
66+
}
7167

72-
return [userID, hash];
73-
}
68+
const secretBuffer = Buffer.from(secretPart, "hex");
69+
const hash = Buffer.from(
70+
await crypto.subtle.digest(ALGORITHM, secretBuffer),
71+
);
7472

75-
/**
76-
* @returns user ID if valid
77-
*/
78-
export async function validateToken(
79-
db: Pool,
80-
token: string,
81-
): Promise<string | null> {
82-
const key: [bigint, Buffer] | null = await tokenKey(token);
83-
84-
if (key === null) {
85-
return null;
73+
return [userID, hash];
8674
}
8775

88-
const result = await db.query(
89-
`
90-
SELECT "userID", "expiresAt"
91-
FROM "api_tokens"
92-
WHERE "userID" = $1 AND "hash" = $2
93-
`,
94-
key,
95-
);
96-
97-
if (result.rowCount !== 1) {
98-
return null;
76+
/**
77+
* @returns user ID if valid
78+
*/
79+
export async function validate(
80+
db: Pool,
81+
token: string,
82+
): Promise<string | null> {
83+
const key: [bigint, Buffer] | null = await tokenKey(token);
84+
85+
if (key === null) {
86+
return null;
87+
}
88+
89+
const result = await db.query(
90+
`
91+
SELECT "userID", "expiresAt"
92+
FROM "api_tokens"
93+
WHERE "userID" = $1 AND "hash" = $2
94+
`,
95+
key,
96+
);
97+
98+
if (result.rowCount !== 1) {
99+
return null;
100+
}
101+
102+
const { expiresAt, userID } = dbParse(TokenInfo, result.rows[0]);
103+
104+
if (Date.now() >= expiresAt.getTime()) {
105+
await db.query(
106+
`
107+
DELETE FROM "api_tokens"
108+
WHERE "userID" = $1 AND "hash" = $2
109+
`,
110+
key,
111+
);
112+
return null;
113+
}
114+
115+
if (Date.now() - expiresAt.getTime() >= TOKEN_REFRESH_THRESHOLD) {
116+
await db.query(
117+
`
118+
UPDATE "api_tokens"
119+
SET "expiresAt" = $1
120+
WHERE "userID" = $2 AND "hash" = $3
121+
`,
122+
[new Date(Date.now() + TOKEN_LIFETIME), userID, key[1]],
123+
);
124+
}
125+
126+
return userID;
99127
}
100128

101-
const { expiresAt, userID } = dbParse(TokenInfo, result.rows[0]);
129+
export async function remove(db: Pool, token: string): Promise<boolean> {
130+
const key = await tokenKey(token);
102131

103-
if (Date.now() >= expiresAt.getTime()) {
104-
await db.query(
132+
if (key === null) {
133+
return false;
134+
}
135+
136+
const result = await db.query(
105137
`
106138
DELETE FROM "api_tokens"
107139
WHERE "userID" = $1 AND "hash" = $2
108140
`,
109141
key,
110142
);
111-
return null;
143+
144+
return result.rowCount === 1;
112145
}
113146

114-
if (Date.now() - expiresAt.getTime() >= TOKEN_REFRESH_THRESHOLD) {
115-
await db.query(
147+
export async function removeExpiredTokens(db: Pool): Promise<number> {
148+
const result = await db.query(
116149
`
117-
UPDATE "api_tokens"
118-
SET "expiresAt" = $1
119-
WHERE "userID" = $2 AND "hash" = $3
150+
DELETE FROM "api_tokens"
151+
WHERE "expiresAt" <= $1
120152
`,
121-
[new Date(Date.now() + TOKEN_LIFETIME), userID, key[1]],
153+
[new Date()],
122154
);
123-
}
124-
125-
return userID;
126-
}
127-
128-
export async function deleteToken(db: Pool, token: string): Promise<boolean> {
129-
const key = await tokenKey(token);
130155

131-
if (key === null) {
132-
return false;
156+
return result.rowCount ?? 0;
133157
}
134-
135-
const result = await db.query(
136-
`
137-
DELETE FROM "api_tokens"
138-
WHERE "userID" = $1 AND "hash" = $2
139-
`,
140-
key,
141-
);
142-
143-
return result.rowCount === 1;
144-
}
145-
146-
export async function deleteExpiredTokens(db: Pool): Promise<number> {
147-
const result = await db.query(
148-
`
149-
DELETE FROM "api_tokens"
150-
WHERE "expiresAt" <= $1
151-
`,
152-
[new Date()],
153-
);
154-
155-
return result.rowCount ?? 0;
156158
}

0 commit comments

Comments
 (0)