feat(syncServer): add verification resend count to user model and update logic for resending verification emails

This commit is contained in:
Johannes Millan 2025-11-27 20:49:05 +01:00
parent da1e4dcada
commit f0ea4c6b2d
2 changed files with 67 additions and 7 deletions

View file

@ -51,11 +51,58 @@ export const registerUser = async (
throw new Error('Failed to send verification email. Please try again later.');
}
} catch (err: any) {
// If unique constraint violation (user exists), we swallow the error
// to prevent email enumeration.
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
Logger.info(`Registration attempt for existing email: ${email}`);
// In a real system, we might want to send a "You already have an account" email here
const existingUser = db
.prepare('SELECT * FROM users WHERE email = ?')
.get(email) as User | undefined;
if (!existingUser) {
Logger.warn(`Unique constraint hit but user not found for email: ${email}`);
} else if (existingUser.is_verified === 1) {
Logger.info(`Registration attempt for verified email: ${email}`);
} else if (existingUser.verification_resend_count >= 1) {
Logger.info(`Verification resend already sent for email: ${email}`);
} else {
const tokenStillValid =
!!existingUser.verification_token &&
!!existingUser.verification_token_expires_at &&
existingUser.verification_token_expires_at > Date.now();
const newToken =
tokenStillValid && existingUser.verification_token
? existingUser.verification_token
: randomBytes(32).toString('hex');
const newExpiresAt = tokenStillValid
? existingUser.verification_token_expires_at
: Date.now() + TWENTY_FOUR_HOURS_MS;
const previousToken = existingUser.verification_token;
const previousExpiresAt = existingUser.verification_token_expires_at;
const previousResendCount = existingUser.verification_resend_count;
db.prepare(
`
UPDATE users
SET verification_token = ?, verification_token_expires_at = ?, verification_resend_count = verification_resend_count + 1
WHERE id = ?
`,
).run(newToken, newExpiresAt, existingUser.id);
const emailSent = await sendVerificationEmail(email, newToken);
if (!emailSent) {
db.prepare(
`
UPDATE users
SET verification_token = ?, verification_token_expires_at = ?, verification_resend_count = ?
WHERE id = ?
`,
).run(previousToken, previousExpiresAt, previousResendCount, existingUser.id);
throw new Error('Failed to send verification email. Please try again later.');
}
Logger.info(`Resent verification email for: ${email}`);
}
} else {
throw err;
}
@ -85,7 +132,11 @@ export const verifyEmail = (token: string): boolean => {
}
db.prepare(
'UPDATE users SET is_verified = 1, verification_token = NULL, verification_token_expires_at = NULL WHERE id = ?',
`
UPDATE users
SET is_verified = 1, verification_token = NULL, verification_token_expires_at = NULL, verification_resend_count = 0
WHERE id = ?
`,
).run(user.id);
Logger.info(`User verified: ${user.email}`);

View file

@ -10,12 +10,13 @@ export interface User {
is_verified: number; // 0 or 1
verification_token: string | null;
verification_token_expires_at: number | null; // Unix timestamp
verification_resend_count: number; // number of times verification mail was resent
created_at: string;
}
let db: Database.Database;
export const initDb = (dataDir: string) => {
export const initDb = (dataDir: string): void => {
const dbPath = path.join(dataDir, 'database.sqlite');
// Ensure data directory exists
@ -34,6 +35,7 @@ export const initDb = (dataDir: string) => {
is_verified INTEGER DEFAULT 0,
verification_token TEXT,
verification_token_expires_at INTEGER,
verification_resend_count INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
@ -56,10 +58,17 @@ export const initDb = (dataDir: string) => {
db.exec('ALTER TABLE users ADD COLUMN verification_token_expires_at INTEGER');
}
const hasResendCount = columns.some((col) => col.name === 'verification_resend_count');
if (!hasResendCount) {
Logger.info('Migrating database: adding verification_resend_count column');
db.exec('ALTER TABLE users ADD COLUMN verification_resend_count INTEGER DEFAULT 0');
}
Logger.info(`Database initialized at ${dbPath}`);
};
export const getDb = () => {
export const getDb = (): Database.Database => {
if (!db) {
throw new Error('Database not initialized');
}