kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { createHash, randomUUID } from "node:crypto";
2import { createId } from "@paralleldrive/cuid2";
3import db from "../database";
4import { sessionTable } from "../database/schema";
5
6type RegisteredClient = {
7 clientId: string;
8 redirectUris: string[];
9 clientName?: string;
10 issuedAt: number;
11};
12
13type AuthCode = {
14 clientId: string;
15 userId: string;
16 codeChallenge: string;
17 redirectUri: string;
18 expiresAt: number;
19};
20
21const clients = new Map<string, RegisteredClient>();
22const codes = new Map<string, AuthCode>();
23
24export function getClient(clientId: string): RegisteredClient | undefined {
25 return clients.get(clientId);
26}
27
28export function registerClient(params: {
29 redirectUris: string[];
30 clientName?: string;
31}): RegisteredClient {
32 const clientId = randomUUID();
33 const client: RegisteredClient = {
34 clientId,
35 redirectUris: params.redirectUris,
36 clientName: params.clientName,
37 issuedAt: Math.floor(Date.now() / 1000),
38 };
39 clients.set(clientId, client);
40 return client;
41}
42
43export function createAuthCode(params: {
44 clientId: string;
45 userId: string;
46 codeChallenge: string;
47 redirectUri: string;
48}): string {
49 const code = randomUUID();
50 codes.set(code, {
51 ...params,
52 expiresAt: Date.now() + 5 * 60 * 1000,
53 });
54 return code;
55}
56
57function base64url(buf: Buffer): string {
58 return buf.toString("base64url");
59}
60
61function verifyPkce(codeVerifier: string, codeChallenge: string): boolean {
62 const hash = createHash("sha256").update(codeVerifier).digest();
63 return base64url(hash) === codeChallenge;
64}
65
66export async function exchangeCode(
67 code: string,
68 clientId: string,
69 codeVerifier: string,
70 redirectUri: string,
71): Promise<{ accessToken: string; expiresIn: number } | null> {
72 const stored = codes.get(code);
73 if (!stored) return null;
74 codes.delete(code);
75
76 if (stored.clientId !== clientId) return null;
77 if (stored.redirectUri !== redirectUri) return null;
78 if (stored.expiresAt < Date.now()) return null;
79 if (!verifyPkce(codeVerifier, stored.codeChallenge)) return null;
80
81 const sessionToken = randomUUID();
82 const expiresIn = 30 * 24 * 60 * 60;
83
84 await db.insert(sessionTable).values({
85 id: createId(),
86 token: sessionToken,
87 userId: stored.userId,
88 expiresAt: new Date(Date.now() + expiresIn * 1000),
89 createdAt: new Date(),
90 updatedAt: new Date(),
91 });
92
93 return { accessToken: sessionToken, expiresIn };
94}