the best lightweight web dev stack built on bun
at main 8.9 kB view raw
1import indexHTML from "./pages/index.html"; 2import { 3 createSession, 4 createUser, 5 deleteSession, 6 getUserBySession, 7 getUserByUsername, 8 getSessionFromRequest, 9} from "./lib/auth"; 10import { 11 createAuthenticationOptions, 12 createRegistrationOptions, 13 deletePasskey, 14 getPasskeysForUser, 15 updatePasskeyName, 16 verifyAndAuthenticatePasskey, 17 verifyAndCreatePasskey, 18} from "./lib/passkey"; 19import { requireAuth } from "./lib/middleware"; 20import { 21 decrementCounter, 22 getCounterForUser, 23 incrementCounter, 24 resetCounter, 25} from "./lib/counter"; 26 27const port = 3000; 28 29Bun.serve({ 30 port, 31 routes: { 32 "/": indexHTML, 33 34 // Auth endpoints 35 "/api/auth/me": { 36 GET: (req) => { 37 try { 38 const sessionId = getSessionFromRequest(req); 39 if (!sessionId) { 40 return new Response( 41 JSON.stringify({ error: "Not authenticated" }), 42 { 43 status: 401, 44 }, 45 ); 46 } 47 48 const user = getUserBySession(sessionId); 49 if (!user) { 50 return new Response(JSON.stringify({ error: "Invalid session" }), { 51 status: 401, 52 }); 53 } 54 55 return new Response(JSON.stringify(user), { 56 headers: { "Content-Type": "application/json" }, 57 }); 58 } catch (error) { 59 return new Response( 60 JSON.stringify({ 61 error: error instanceof Error ? error.message : "Unknown error", 62 }), 63 { status: 500 }, 64 ); 65 } 66 }, 67 }, 68 69 "/api/auth/check-email": { 70 GET: (req) => { 71 try { 72 const url = new URL(req.url); 73 const username = url.searchParams.get("username"); 74 75 if (!username) { 76 return new Response( 77 JSON.stringify({ error: "Username required" }), 78 { 79 status: 400, 80 }, 81 ); 82 } 83 84 const existing = getUserByUsername(username); 85 if (existing) { 86 return new Response( 87 JSON.stringify({ error: "Username already taken" }), 88 { status: 400 }, 89 ); 90 } 91 92 return new Response(JSON.stringify({ available: true }), { 93 headers: { "Content-Type": "application/json" }, 94 }); 95 } catch (error) { 96 return new Response( 97 JSON.stringify({ 98 error: error instanceof Error ? error.message : "Unknown error", 99 }), 100 { status: 500 }, 101 ); 102 } 103 }, 104 }, 105 106 "/api/auth/register": { 107 POST: async (req) => { 108 try { 109 const body = await req.json(); 110 const { username, credential, challenge } = body; 111 112 if (!username || !credential || !challenge) { 113 return new Response( 114 JSON.stringify({ 115 error: "Username, credential, and challenge required", 116 }), 117 { status: 400 }, 118 ); 119 } 120 121 // Check if user already exists 122 const existing = getUserByUsername(username); 123 if (existing) { 124 return new Response( 125 JSON.stringify({ error: "Username already taken" }), 126 { status: 400 }, 127 ); 128 } 129 130 // Create user 131 const user = await createUser(username); 132 133 // Verify and create passkey 134 await verifyAndCreatePasskey(user.id, credential, challenge); 135 136 // Create session 137 const sessionId = createSession(user.id); 138 139 return new Response(JSON.stringify(user), { 140 headers: { 141 "Content-Type": "application/json", 142 "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${7 * 24 * 60 * 60}`, 143 }, 144 }); 145 } catch (error) { 146 return new Response( 147 JSON.stringify({ 148 error: error instanceof Error ? error.message : "Unknown error", 149 }), 150 { status: 500 }, 151 ); 152 } 153 }, 154 }, 155 156 "/api/auth/logout": { 157 POST: (req) => { 158 try { 159 const sessionId = getSessionFromRequest(req); 160 if (sessionId) { 161 deleteSession(sessionId); 162 } 163 164 return new Response(JSON.stringify({ success: true }), { 165 headers: { 166 "Content-Type": "application/json", 167 "Set-Cookie": 168 "session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0", 169 }, 170 }); 171 } catch (error) { 172 return new Response( 173 JSON.stringify({ 174 error: error instanceof Error ? error.message : "Unknown error", 175 }), 176 { status: 500 }, 177 ); 178 } 179 }, 180 }, 181 182 // Passkey endpoints 183 "/api/auth/passkey/register/options": { 184 GET: async (req) => { 185 try { 186 // For registration, we need username from query params (no session yet) 187 const url = new URL(req.url); 188 const username = url.searchParams.get("username"); 189 190 if (!username) { 191 return new Response( 192 JSON.stringify({ error: "Username required" }), 193 { 194 status: 400, 195 }, 196 ); 197 } 198 199 // Create temporary user object for registration options 200 const tempUser = { 201 id: 0, // Temporary ID 202 username, 203 name: null, 204 avatar: "temp", 205 created_at: Math.floor(Date.now() / 1000), 206 }; 207 208 const options = await createRegistrationOptions(tempUser); 209 210 return new Response(JSON.stringify(options), { 211 headers: { "Content-Type": "application/json" }, 212 }); 213 } catch (error) { 214 return new Response( 215 JSON.stringify({ 216 error: error instanceof Error ? error.message : "Unknown error", 217 }), 218 { status: 500 }, 219 ); 220 } 221 }, 222 }, 223 224 "/api/auth/passkey/authenticate/options": { 225 GET: async (req) => { 226 try { 227 const options = await createAuthenticationOptions(); 228 229 return new Response(JSON.stringify(options), { 230 headers: { "Content-Type": "application/json" }, 231 }); 232 } catch (error) { 233 return new Response( 234 JSON.stringify({ 235 error: error instanceof Error ? error.message : "Unknown error", 236 }), 237 { status: 500 }, 238 ); 239 } 240 }, 241 }, 242 243 "/api/auth/passkey/authenticate/verify": { 244 POST: async (req) => { 245 try { 246 const body = await req.json(); 247 const { credential, challenge } = body; 248 249 if (!credential || !challenge) { 250 return new Response( 251 JSON.stringify({ error: "Credential and challenge required" }), 252 { status: 400 }, 253 ); 254 } 255 256 const { userId } = await verifyAndAuthenticatePasskey( 257 credential, 258 challenge, 259 ); 260 261 const user = getUserBySession( 262 createSession( 263 userId, 264 req.headers.get("x-forwarded-for") || undefined, 265 ), 266 ); 267 268 if (!user) { 269 return new Response(JSON.stringify({ error: "User not found" }), { 270 status: 404, 271 }); 272 } 273 274 const sessionId = createSession(userId); 275 276 return new Response(JSON.stringify(user), { 277 headers: { 278 "Content-Type": "application/json", 279 "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${7 * 24 * 60 * 60}`, 280 }, 281 }); 282 } catch (error) { 283 return new Response( 284 JSON.stringify({ 285 error: error instanceof Error ? error.message : "Unknown error", 286 }), 287 { status: 500 }, 288 ); 289 } 290 }, 291 }, 292 293 // Counter endpoints 294 "/api/counter": { 295 GET: async (req) => { 296 try { 297 const userId = await requireAuth(req); 298 const count = getCounterForUser(userId); 299 300 return new Response(JSON.stringify({ count }), { 301 headers: { "Content-Type": "application/json" }, 302 }); 303 } catch (error) { 304 return new Response( 305 JSON.stringify({ 306 error: 307 error instanceof Error ? error.message : "Not authenticated", 308 }), 309 { status: 401 }, 310 ); 311 } 312 }, 313 }, 314 315 "/api/counter/increment": { 316 POST: async (req) => { 317 try { 318 const userId = await requireAuth(req); 319 const count = incrementCounter(userId); 320 321 return new Response(JSON.stringify({ count }), { 322 headers: { "Content-Type": "application/json" }, 323 }); 324 } catch (error) { 325 return new Response( 326 JSON.stringify({ 327 error: 328 error instanceof Error ? error.message : "Not authenticated", 329 }), 330 { status: 401 }, 331 ); 332 } 333 }, 334 }, 335 336 "/api/counter/decrement": { 337 POST: async (req) => { 338 try { 339 const userId = await requireAuth(req); 340 const count = decrementCounter(userId); 341 342 return new Response(JSON.stringify({ count }), { 343 headers: { "Content-Type": "application/json" }, 344 }); 345 } catch (error) { 346 return new Response( 347 JSON.stringify({ 348 error: 349 error instanceof Error ? error.message : "Not authenticated", 350 }), 351 { status: 401 }, 352 ); 353 } 354 }, 355 }, 356 357 "/api/counter/reset": { 358 POST: async (req) => { 359 try { 360 const userId = await requireAuth(req); 361 resetCounter(userId); 362 363 return new Response(JSON.stringify({ count: 0 }), { 364 headers: { "Content-Type": "application/json" }, 365 }); 366 } catch (error) { 367 return new Response( 368 JSON.stringify({ 369 error: 370 error instanceof Error ? error.message : "Not authenticated", 371 }), 372 { status: 401 }, 373 ); 374 } 375 }, 376 }, 377 }, 378 development: { 379 hmr: true, 380 console: true, 381 }, 382}); 383 384console.log(`🥞 Tacy Stack running at http://localhost:${port}`);