🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: handle null createdAt case in email verification

+97 -4
+84 -2
src/index.ts
··· 28 28 verifyEmailToken, 29 29 verifyEmailCode, 30 30 isEmailVerified, 31 + getVerificationCodeSentAt, 31 32 createPasswordResetToken, 32 33 verifyPasswordResetToken, 33 34 consumePasswordResetToken, ··· 267 268 const user = await createUser(email, password, name); 268 269 269 270 // Send verification email - MUST succeed for registration to complete 270 - const { code, token } = createEmailVerificationToken(user.id); 271 + const { code, token, sentAt } = createEmailVerificationToken(user.id); 271 272 272 273 try { 273 274 await sendEmail({ ··· 309 310 { 310 311 user: { id: user.id, email: user.email }, 311 312 email_verification_required: true, 313 + verification_code_sent_at: sentAt, 312 314 }, 313 315 { status: 200 }, 314 316 ); ··· 320 322 { status: 400 }, 321 323 ); 322 324 } 325 + console.error("[Auth] Registration error:", err); 323 326 return Response.json( 324 327 { error: "Registration failed" }, 325 328 { status: 500 }, ··· 370 373 371 374 // Check if email is verified 372 375 if (!isEmailVerified(user.id)) { 376 + let codeSentAt = getVerificationCodeSentAt(user.id); 377 + 378 + // If no verification code exists, auto-send one 379 + if (!codeSentAt) { 380 + const { code, token, sentAt } = createEmailVerificationToken(user.id); 381 + codeSentAt = sentAt; 382 + 383 + try { 384 + await sendEmail({ 385 + to: user.email, 386 + subject: "Verify your email - Thistle", 387 + html: verifyEmailTemplate({ 388 + name: user.name, 389 + code, 390 + token, 391 + }), 392 + }); 393 + } catch (err) { 394 + console.error("[Email] Failed to send verification email on login:", err); 395 + // Don't fail login - just return null timestamp so client can try resend 396 + codeSentAt = null; 397 + } 398 + } 399 + 373 400 return Response.json( 374 401 { 375 402 user: { id: user.id, email: user.email }, 376 403 email_verification_required: true, 404 + verification_code_sent_at: codeSentAt, 377 405 }, 378 406 { status: 200 }, 379 407 ); ··· 389 417 }, 390 418 }, 391 419 ); 392 - } catch { 420 + } catch (error) { 421 + console.error("[Auth] Login error:", error); 393 422 return Response.json({ error: "Login failed" }, { status: 500 }); 394 423 } 395 424 }, ··· 527 556 }); 528 557 529 558 return Response.json({ message: "Verification email sent" }); 559 + } catch (error) { 560 + return handleError(error); 561 + } 562 + }, 563 + }, 564 + "/api/auth/resend-verification-code": { 565 + POST: async (req) => { 566 + try { 567 + const body = await req.json(); 568 + const { email } = body; 569 + 570 + if (!email) { 571 + return Response.json({ error: "Email required" }, { status: 400 }); 572 + } 573 + 574 + // Rate limiting by email 575 + const rateLimitError = enforceRateLimit(req, "resend-verification-code", { 576 + account: { max: 3, windowSeconds: 5 * 60, email }, 577 + }); 578 + if (rateLimitError) return rateLimitError; 579 + 580 + // Get user by email 581 + const user = getUserByEmail(email); 582 + if (!user) { 583 + // Don't reveal if user exists 584 + return Response.json({ message: "If an account exists with that email, a verification code has been sent" }); 585 + } 586 + 587 + // Check if already verified 588 + if (isEmailVerified(user.id)) { 589 + return Response.json( 590 + { error: "Email already verified" }, 591 + { status: 400 }, 592 + ); 593 + } 594 + 595 + // Generate new code and send email 596 + const { code, token, sentAt } = createEmailVerificationToken(user.id); 597 + 598 + await sendEmail({ 599 + to: user.email, 600 + subject: "Verify your email - Thistle", 601 + html: verifyEmailTemplate({ 602 + name: user.name, 603 + code, 604 + token, 605 + }), 606 + }); 607 + 608 + return Response.json({ 609 + message: "Verification code sent", 610 + verification_code_sent_at: sentAt, 611 + }); 530 612 } catch (error) { 531 613 return handleError(error); 532 614 }
+13 -2
src/lib/auth.ts
··· 257 257 * Email verification functions 258 258 */ 259 259 260 - export function createEmailVerificationToken(userId: number): { code: string; token: string } { 260 + export function createEmailVerificationToken(userId: number): { code: string; token: string; sentAt: number } { 261 261 // Generate a 6-digit code for user to enter 262 262 const code = Math.floor(100000 + Math.random() * 900000).toString(); 263 263 const id = crypto.randomUUID(); 264 264 const token = crypto.randomUUID(); // Separate token for URL 265 265 const expiresAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; // 24 hours 266 + const sentAt = Math.floor(Date.now() / 1000); // Timestamp when code is created 266 267 267 268 // Delete any existing tokens for this user 268 269 db.run("DELETE FROM email_verification_tokens WHERE user_id = ?", [userId]); ··· 279 280 [crypto.randomUUID(), userId, token, expiresAt], 280 281 ); 281 282 282 - return { code, token }; 283 + return { code, token, sentAt }; 283 284 } 284 285 285 286 export function verifyEmailToken( ··· 346 347 .get(userId); 347 348 348 349 return result?.email_verified === 1; 350 + } 351 + 352 + export function getVerificationCodeSentAt(userId: number): number | null { 353 + const result = db 354 + .query<{ created_at: number }, [number]>( 355 + "SELECT MAX(created_at) as created_at FROM email_verification_tokens WHERE user_id = ?", 356 + ) 357 + .get(userId); 358 + 359 + return result?.created_at ?? null; 349 360 } 350 361 351 362 /**