🪻 distributed transcription service thistle.dunkirk.sh

feat: handle null createdAt case in email verification

dunkirk.sh ec5cb8d4 d206be10

verified
Changed files
+97 -4
src
+84 -2
src/index.ts
···
verifyEmailToken,
verifyEmailCode,
isEmailVerified,
+
getVerificationCodeSentAt,
createPasswordResetToken,
verifyPasswordResetToken,
consumePasswordResetToken,
···
const user = await createUser(email, password, name);
// Send verification email - MUST succeed for registration to complete
-
const { code, token } = createEmailVerificationToken(user.id);
+
const { code, token, sentAt } = createEmailVerificationToken(user.id);
try {
await sendEmail({
···
{
user: { id: user.id, email: user.email },
email_verification_required: true,
+
verification_code_sent_at: sentAt,
},
{ status: 200 },
);
···
{ status: 400 },
);
}
+
console.error("[Auth] Registration error:", err);
return Response.json(
{ error: "Registration failed" },
{ status: 500 },
···
// Check if email is verified
if (!isEmailVerified(user.id)) {
+
let codeSentAt = getVerificationCodeSentAt(user.id);
+
+
// If no verification code exists, auto-send one
+
if (!codeSentAt) {
+
const { code, token, sentAt } = createEmailVerificationToken(user.id);
+
codeSentAt = sentAt;
+
+
try {
+
await sendEmail({
+
to: user.email,
+
subject: "Verify your email - Thistle",
+
html: verifyEmailTemplate({
+
name: user.name,
+
code,
+
token,
+
}),
+
});
+
} catch (err) {
+
console.error("[Email] Failed to send verification email on login:", err);
+
// Don't fail login - just return null timestamp so client can try resend
+
codeSentAt = null;
+
}
+
}
+
return Response.json(
{
user: { id: user.id, email: user.email },
email_verification_required: true,
+
verification_code_sent_at: codeSentAt,
},
{ status: 200 },
);
···
},
},
);
-
} catch {
+
} catch (error) {
+
console.error("[Auth] Login error:", error);
return Response.json({ error: "Login failed" }, { status: 500 });
}
},
···
});
return Response.json({ message: "Verification email sent" });
+
} catch (error) {
+
return handleError(error);
+
}
+
},
+
},
+
"/api/auth/resend-verification-code": {
+
POST: async (req) => {
+
try {
+
const body = await req.json();
+
const { email } = body;
+
+
if (!email) {
+
return Response.json({ error: "Email required" }, { status: 400 });
+
}
+
+
// Rate limiting by email
+
const rateLimitError = enforceRateLimit(req, "resend-verification-code", {
+
account: { max: 3, windowSeconds: 5 * 60, email },
+
});
+
if (rateLimitError) return rateLimitError;
+
+
// Get user by email
+
const user = getUserByEmail(email);
+
if (!user) {
+
// Don't reveal if user exists
+
return Response.json({ message: "If an account exists with that email, a verification code has been sent" });
+
}
+
+
// Check if already verified
+
if (isEmailVerified(user.id)) {
+
return Response.json(
+
{ error: "Email already verified" },
+
{ status: 400 },
+
);
+
}
+
+
// Generate new code and send email
+
const { code, token, sentAt } = createEmailVerificationToken(user.id);
+
+
await sendEmail({
+
to: user.email,
+
subject: "Verify your email - Thistle",
+
html: verifyEmailTemplate({
+
name: user.name,
+
code,
+
token,
+
}),
+
});
+
+
return Response.json({
+
message: "Verification code sent",
+
verification_code_sent_at: sentAt,
+
});
} catch (error) {
return handleError(error);
}
+13 -2
src/lib/auth.ts
···
* Email verification functions
*/
-
export function createEmailVerificationToken(userId: number): { code: string; token: string } {
+
export function createEmailVerificationToken(userId: number): { code: string; token: string; sentAt: number } {
// Generate a 6-digit code for user to enter
const code = Math.floor(100000 + Math.random() * 900000).toString();
const id = crypto.randomUUID();
const token = crypto.randomUUID(); // Separate token for URL
const expiresAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; // 24 hours
+
const sentAt = Math.floor(Date.now() / 1000); // Timestamp when code is created
// Delete any existing tokens for this user
db.run("DELETE FROM email_verification_tokens WHERE user_id = ?", [userId]);
···
[crypto.randomUUID(), userId, token, expiresAt],
);
-
return { code, token };
+
return { code, token, sentAt };
}
export function verifyEmailToken(
···
.get(userId);
return result?.email_verified === 1;
+
}
+
+
export function getVerificationCodeSentAt(userId: number): number | null {
+
const result = db
+
.query<{ created_at: number }, [number]>(
+
"SELECT MAX(created_at) as created_at FROM email_verification_tokens WHERE user_id = ?",
+
)
+
.get(userId);
+
+
return result?.created_at ?? null;
}
/**