the best lightweight web dev stack built on bun
1import { eq } from "drizzle-orm";
2import db from "../db/db";
3import { users, sessions, type User } from "../db/schema";
4
5const SESSION_DURATION = 7 * 24 * 60 * 60; // 7 days in seconds
6
7export interface User {
8 id: number;
9 username: string;
10 name: string | null;
11 avatar: string;
12 created_at: number;
13}
14
15export interface Session {
16 id: string;
17 user_id: number;
18 ip_address: string | null;
19 user_agent: string | null;
20 created_at: number;
21 expires_at: number;
22}
23
24export function createSession(
25 userId: number,
26 ipAddress?: string,
27 userAgent?: string,
28): string {
29 const sessionId = crypto.randomUUID();
30 const expiresAt = Math.floor(Date.now() / 1000) + SESSION_DURATION;
31
32 db.insert(sessions)
33 .values({
34 id: sessionId,
35 user_id: userId,
36 ip_address: ipAddress ?? null,
37 user_agent: userAgent ?? null,
38 expires_at: new Date(expiresAt * 1000),
39 })
40 .run();
41
42 return sessionId;
43}
44
45export function getSession(sessionId: string): Session | null {
46 const now = Math.floor(Date.now() / 1000);
47
48 const session = db
49 .select()
50 .from(sessions)
51 .where(eq(sessions.id, sessionId))
52 .get();
53
54 if (!session || Math.floor(session.expires_at.getTime() / 1000) <= now) {
55 return null;
56 }
57
58 return {
59 id: session.id,
60 user_id: session.user_id,
61 ip_address: session.ip_address,
62 user_agent: session.user_agent,
63 created_at: Math.floor(session.created_at.getTime() / 1000),
64 expires_at: Math.floor(session.expires_at.getTime() / 1000),
65 };
66}
67
68export function getUserBySession(sessionId: string): User | null {
69 const session = getSession(sessionId);
70 if (!session) return null;
71
72 const user = db
73 .select()
74 .from(users)
75 .where(eq(users.id, session.user_id))
76 .get();
77
78 if (!user) return null;
79
80 return {
81 id: user.id,
82 username: user.username,
83 name: user.name,
84 avatar: user.avatar,
85 created_at: Math.floor(user.created_at.getTime() / 1000),
86 };
87}
88
89export function getUserByUsername(username: string): User | null {
90 const user = db
91 .select()
92 .from(users)
93 .where(eq(users.username, username))
94 .get();
95
96 if (!user) return null;
97
98 return {
99 id: user.id,
100 username: user.username,
101 name: user.name,
102 avatar: user.avatar,
103 created_at: Math.floor(user.created_at.getTime() / 1000),
104 };
105}
106
107export function deleteSession(sessionId: string): void {
108 db.delete(sessions).where(eq(sessions.id, sessionId)).run();
109}
110
111export async function createUser(
112 username: string,
113 name?: string,
114): Promise<User> {
115 // Generate deterministic avatar from username
116 const encoder = new TextEncoder();
117 const data = encoder.encode(username.toLowerCase());
118 const hashBuffer = await crypto.subtle.digest("SHA-256", data);
119 const hashArray = Array.from(new Uint8Array(hashBuffer));
120 const avatar = hashArray
121 .map((b) => b.toString(16).padStart(2, "0"))
122 .join("")
123 .substring(0, 16);
124
125 const result = db
126 .insert(users)
127 .values({
128 username,
129 name: name ?? null,
130 avatar,
131 })
132 .run();
133
134 const user = db
135 .select()
136 .from(users)
137 .where(eq(users.id, Number(result.lastInsertRowid)))
138 .get();
139
140 if (!user) {
141 throw new Error("Failed to create user");
142 }
143
144 return {
145 id: user.id,
146 username: user.username,
147 name: user.name,
148 avatar: user.avatar,
149 created_at: Math.floor(user.created_at.getTime() / 1000),
150 };
151}
152
153export function getSessionFromRequest(req: Request): string | null {
154 const cookie = req.headers.get("cookie");
155 if (!cookie) return null;
156
157 const sessionMatch = cookie.match(/session=([^;]+)/);
158 return sessionMatch ? sessionMatch[1] : null;
159}