← back to NationalPaperHangers
Reject malformed emails at signup and claim instead of just checking for an @
50d107f1c77d41565ab8442e7643d4941e66a34c · 2026-05-19 07:55:39 -0700 · Steve
Files touched
M lib/utils.jsM routes/auth.jsM routes/claim.js
Diff
commit 50d107f1c77d41565ab8442e7643d4941e66a34c
Author: Steve <steve@designerwallcoverings.com>
Date: Tue May 19 07:55:39 2026 -0700
Reject malformed emails at signup and claim instead of just checking for an @
---
lib/utils.js | 12 +++++++++++-
routes/auth.js | 3 ++-
routes/claim.js | 4 ++--
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/lib/utils.js b/lib/utils.js
index dc6d98e..1246863 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -40,4 +40,14 @@ function clockMinutes(v) {
return h * 60 + Number(m[2]) + (m[3] ? Number(m[3]) / 60 : 0);
}
-module.exports = { escapeHtml, safeHttpUrl, clampLen, clockMinutes };
+// Single source of truth for "is this a usable email address". Mirrors the
+// shape check the booking endpoint already uses, so signup, claim, and
+// booking all reject the same garbage (e.g. "@", "a@", "a@b") instead of
+// letting it into the installers.email / bookings.customer_email columns.
+function isValidEmail(v) {
+ const s = String(v == null ? '' : v).trim();
+ if (!s || s.length > 254) return false;
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
+}
+
+module.exports = { escapeHtml, safeHttpUrl, clampLen, clockMinutes, isValidEmail };
diff --git a/routes/auth.js b/routes/auth.js
index 36ffc51..846f407 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -2,6 +2,7 @@ const express = require('express');
const slugify = require('slugify');
const db = require('../lib/db');
const { hashPassword, verifyPassword } = require('../lib/auth');
+const { isValidEmail } = require('../lib/utils');
const router = express.Router();
const GENERIC_LOGIN_ERROR = 'Invalid email or password';
@@ -72,7 +73,7 @@ router.post('/signup', async (req, res, next) => {
const zip = (f.zip || '').trim();
const errs = [];
- if (!email || !email.includes('@')) errs.push('Valid email required');
+ if (!isValidEmail(email)) errs.push('Valid email required');
if (password.length < 8) errs.push('Password must be at least 8 characters');
if (!businessName) errs.push('Business name required');
if (!city) errs.push('City required');
diff --git a/routes/claim.js b/routes/claim.js
index 6123c07..865ab0c 100644
--- a/routes/claim.js
+++ b/routes/claim.js
@@ -15,7 +15,7 @@ const express = require('express');
const crypto = require('crypto');
const db = require('../lib/db');
const email = require('../lib/email');
-const { escapeHtml } = require('../lib/utils');
+const { escapeHtml, isValidEmail } = require('../lib/utils');
const router = express.Router();
@@ -171,7 +171,7 @@ router.post('/installer/:slug/claim/complete', async (req, res, next) => {
const newEmail = (req.body.email || '').toLowerCase().trim();
const password = req.body.password || '';
- if (!newEmail.includes('@') || password.length < 8) {
+ if (!isValidEmail(newEmail) || password.length < 8) {
return res.status(400).render('public/claim-complete', { title: 'Finish claim', installer, error: 'Email and 8+ char password required' });
}
const dupe = await db.one('SELECT id FROM installers WHERE email=$1 AND id<>$2', [newEmail, installer.id]);
← a3b0896 Validate admin portfolio uploads by magic bytes, not the spo
·
back to NationalPaperHangers
·
Coerce a garbage COI project-value to null instead of 500ing 124a38f →