feat(email): Adding email options for registration
This commit is contained in:
@@ -1,8 +1,20 @@
|
||||
import express from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { JWT_SECRET, authMiddleware } from "../middleware/auth.js";
|
||||
import { createUser, getUserByUsername, getUserById } from "../db/usersDb.js";
|
||||
import {
|
||||
createUser,
|
||||
getUserByUsername,
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
createEmailToken,
|
||||
getEmailToken,
|
||||
deleteEmailToken,
|
||||
markEmailVerified,
|
||||
updatePassword,
|
||||
} from "../db/usersDb.js";
|
||||
import { sendConfirmationEmail, sendPasswordResetEmail } from "../email.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -14,6 +26,10 @@ function issueToken(user) {
|
||||
);
|
||||
}
|
||||
|
||||
function generateToken() {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
// POST /api/auth/register
|
||||
router.post("/register", async (req, res) => {
|
||||
const { username, email, password, team } = req.body ?? {};
|
||||
@@ -32,11 +48,16 @@ router.post("/register", async (req, res) => {
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const user = await createUser(username.trim(), email.trim().toLowerCase(), passwordHash, team);
|
||||
const token = issueToken(user);
|
||||
return res.status(201).json({
|
||||
token,
|
||||
user: { id: user.id, username: user.username, team: user.team, role: user.role },
|
||||
});
|
||||
|
||||
// Create and send confirmation token
|
||||
const token = generateToken();
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
await createEmailToken(user.id, token, "confirm", expiresAt);
|
||||
sendConfirmationEmail(user.email, token).catch((e) =>
|
||||
console.error("[email] Failed to send confirmation email:", e.message)
|
||||
);
|
||||
|
||||
return res.status(201).json({ pending_verification: true });
|
||||
} catch (e) {
|
||||
if (e.code === "23505") {
|
||||
if (e.constraint?.includes("email")) return res.status(409).json({ error: "email_taken" });
|
||||
@@ -56,6 +77,7 @@ router.post("/login", async (req, res) => {
|
||||
if (!user) return res.status(401).json({ error: "invalid_credentials" });
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return res.status(401).json({ error: "invalid_credentials" });
|
||||
if (!user.email_verified) return res.status(403).json({ error: "email_not_verified" });
|
||||
const token = issueToken(user);
|
||||
return res.json({
|
||||
token,
|
||||
@@ -80,4 +102,92 @@ router.get("/me", authMiddleware, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
// POST /api/auth/resend-confirmation
|
||||
router.post("/resend-confirmation", async (req, res) => {
|
||||
const { email } = req.body ?? {};
|
||||
if (!email) return res.status(400).json({ error: "missing_fields" });
|
||||
try {
|
||||
const user = await getUserByEmail(email.trim().toLowerCase());
|
||||
// Always respond OK to avoid user enumeration
|
||||
if (!user || user.email_verified) return res.json({ ok: true });
|
||||
|
||||
const token = generateToken();
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
await createEmailToken(user.id, token, "confirm", expiresAt);
|
||||
sendConfirmationEmail(user.email, token).catch((e) =>
|
||||
console.error("[email] Failed to resend confirmation email:", e.message)
|
||||
);
|
||||
return res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/confirm-email?token=TOKEN
|
||||
router.get("/confirm-email", async (req, res) => {
|
||||
const { token } = req.query;
|
||||
if (!token) return res.redirect("/?confirm_error=invalid");
|
||||
try {
|
||||
const row = await getEmailToken(token, "confirm");
|
||||
if (!row) return res.redirect("/?confirm_error=invalid");
|
||||
if (new Date(row.expires_at) < new Date()) {
|
||||
await deleteEmailToken(token);
|
||||
return res.redirect("/?confirm_error=expired");
|
||||
}
|
||||
await markEmailVerified(row.user_id);
|
||||
await deleteEmailToken(token);
|
||||
return res.redirect("/?email_confirmed=1");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.redirect("/?confirm_error=server_error");
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/forgot-password
|
||||
router.post("/forgot-password", async (req, res) => {
|
||||
const { email } = req.body ?? {};
|
||||
if (!email) return res.status(400).json({ error: "missing_fields" });
|
||||
try {
|
||||
const user = await getUserByEmail(email.trim().toLowerCase());
|
||||
// Always respond OK to avoid user enumeration
|
||||
if (!user) return res.json({ ok: true });
|
||||
|
||||
const token = generateToken();
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
||||
await createEmailToken(user.id, token, "reset", expiresAt);
|
||||
sendPasswordResetEmail(user.email, token).catch((e) =>
|
||||
console.error("[email] Failed to send reset email:", e.message)
|
||||
);
|
||||
return res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/reset-password
|
||||
router.post("/reset-password", async (req, res) => {
|
||||
const { token, password } = req.body ?? {};
|
||||
if (!token || !password) return res.status(400).json({ error: "missing_fields" });
|
||||
if (typeof password !== "string" || password.length < 6) {
|
||||
return res.status(400).json({ error: "password_too_short" });
|
||||
}
|
||||
try {
|
||||
const row = await getEmailToken(token, "reset");
|
||||
if (!row) return res.status(400).json({ error: "invalid_token" });
|
||||
if (new Date(row.expires_at) < new Date()) {
|
||||
await deleteEmailToken(token);
|
||||
return res.status(400).json({ error: "token_expired" });
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
await updatePassword(row.user_id, passwordHash);
|
||||
await deleteEmailToken(token);
|
||||
return res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user