Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s
- Include compiled TypeScript with new /impressum, /privacy, /terms routes - Temporary commit of dist files for Docker deployment
103 lines
5.1 KiB
JavaScript
103 lines
5.1 KiB
JavaScript
import { randomBytes, randomInt } from "crypto";
|
|
import logger from "./logger.js";
|
|
import pool from "./db.js";
|
|
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
const CODE_EXPIRY_MS = 15 * 60 * 1000;
|
|
const MAX_ATTEMPTS = 3;
|
|
export async function createVerification(email, apiKey) {
|
|
// Check for existing unexpired, unverified
|
|
const existing = await pool.query("SELECT * FROM verifications WHERE email = $1 AND verified_at IS NULL AND created_at > NOW() - INTERVAL '24 hours' LIMIT 1", [email]);
|
|
if (existing.rows.length > 0) {
|
|
const r = existing.rows[0];
|
|
return { email: r.email, token: r.token, apiKey: r.api_key, createdAt: r.created_at.toISOString(), verifiedAt: null };
|
|
}
|
|
// Remove old unverified
|
|
await pool.query("DELETE FROM verifications WHERE email = $1 AND verified_at IS NULL", [email]);
|
|
const token = randomBytes(32).toString("hex");
|
|
const now = new Date().toISOString();
|
|
await pool.query("INSERT INTO verifications (email, token, api_key, created_at) VALUES ($1, $2, $3, $4)", [email, token, apiKey, now]);
|
|
return { email, token, apiKey, createdAt: now, verifiedAt: null };
|
|
}
|
|
export function verifyToken(token) {
|
|
// Synchronous wrapper — we'll make it async-compatible
|
|
// Actually need to keep sync for the GET /verify route. Use sync query workaround or refactor.
|
|
// For simplicity, we'll cache verifications in memory too.
|
|
return verifyTokenSync(token);
|
|
}
|
|
// In-memory cache for verifications (loaded on startup, updated on changes)
|
|
let verificationsCache = [];
|
|
export async function loadVerifications() {
|
|
const result = await pool.query("SELECT * FROM verifications");
|
|
verificationsCache = result.rows.map((r) => ({
|
|
email: r.email,
|
|
token: r.token,
|
|
apiKey: r.api_key,
|
|
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
|
verifiedAt: r.verified_at ? (r.verified_at instanceof Date ? r.verified_at.toISOString() : r.verified_at) : null,
|
|
}));
|
|
// Cleanup expired entries every 15 minutes
|
|
setInterval(() => {
|
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
const before = verificationsCache.length;
|
|
verificationsCache = verificationsCache.filter((v) => v.verifiedAt || new Date(v.createdAt).getTime() > cutoff);
|
|
const removed = before - verificationsCache.length;
|
|
if (removed > 0)
|
|
logger.info({ removed }, "Cleaned expired verification cache entries");
|
|
}, 15 * 60 * 1000);
|
|
}
|
|
function verifyTokenSync(token) {
|
|
const v = verificationsCache.find((v) => v.token === token);
|
|
if (!v)
|
|
return { status: "invalid" };
|
|
if (v.verifiedAt)
|
|
return { status: "already_verified", verification: v };
|
|
const age = Date.now() - new Date(v.createdAt).getTime();
|
|
if (age > TOKEN_EXPIRY_MS)
|
|
return { status: "expired" };
|
|
v.verifiedAt = new Date().toISOString();
|
|
// Update DB async
|
|
pool.query("UPDATE verifications SET verified_at = $1 WHERE token = $2", [v.verifiedAt, token]).catch((err) => logger.error({ err }, "Failed to update verification"));
|
|
return { status: "ok", verification: v };
|
|
}
|
|
export async function createPendingVerification(email) {
|
|
await pool.query("DELETE FROM pending_verifications WHERE email = $1", [email]);
|
|
const now = new Date();
|
|
const pending = {
|
|
email,
|
|
code: String(randomInt(100000, 999999)),
|
|
createdAt: now.toISOString(),
|
|
expiresAt: new Date(now.getTime() + CODE_EXPIRY_MS).toISOString(),
|
|
attempts: 0,
|
|
};
|
|
await pool.query("INSERT INTO pending_verifications (email, code, created_at, expires_at, attempts) VALUES ($1, $2, $3, $4, $5)", [pending.email, pending.code, pending.createdAt, pending.expiresAt, pending.attempts]);
|
|
return pending;
|
|
}
|
|
export async function verifyCode(email, code) {
|
|
const cleanEmail = email.trim().toLowerCase();
|
|
const result = await pool.query("SELECT * FROM pending_verifications WHERE email = $1", [cleanEmail]);
|
|
const pending = result.rows[0];
|
|
if (!pending)
|
|
return { status: "invalid" };
|
|
if (new Date() > new Date(pending.expires_at)) {
|
|
await pool.query("DELETE FROM pending_verifications WHERE email = $1", [cleanEmail]);
|
|
return { status: "expired" };
|
|
}
|
|
if (pending.attempts >= MAX_ATTEMPTS) {
|
|
await pool.query("DELETE FROM pending_verifications WHERE email = $1", [cleanEmail]);
|
|
return { status: "max_attempts" };
|
|
}
|
|
await pool.query("UPDATE pending_verifications SET attempts = attempts + 1 WHERE email = $1", [cleanEmail]);
|
|
if (pending.code !== code) {
|
|
return { status: "invalid" };
|
|
}
|
|
await pool.query("DELETE FROM pending_verifications WHERE email = $1", [cleanEmail]);
|
|
return { status: "ok" };
|
|
}
|
|
export async function isEmailVerified(email) {
|
|
const result = await pool.query("SELECT 1 FROM verifications WHERE email = $1 AND verified_at IS NOT NULL LIMIT 1", [email]);
|
|
return result.rows.length > 0;
|
|
}
|
|
export async function getVerifiedApiKey(email) {
|
|
const result = await pool.query("SELECT api_key FROM verifications WHERE email = $1 AND verified_at IS NOT NULL LIMIT 1", [email]);
|
|
return result.rows[0]?.api_key ?? null;
|
|
}
|