← back to NationalPaperHangers

routes/auth.js

117 lines

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';

function safeNext(input) {
  // Only allow same-origin paths; reject //evil.com, /\\foo, full URLs.
  const v = String(input || '').trim();
  if (!v.startsWith('/')) return '/admin';
  if (v.startsWith('//') || v.startsWith('/\\')) return '/admin';
  return v;
}

function regenerateSession(req) {
  return new Promise((resolve, reject) => {
    req.session.regenerate(err => err ? reject(err) : resolve());
  });
}

router.get('/login', (req, res) => {
  res.render('auth/login', {
    title: 'Installer login · National Paper Hangers',
    error: null,
    next: safeNext(req.query.next || '/admin')
  });
});

router.post('/login', async (req, res, next) => {
  try {
    const email = (req.body.email || '').toLowerCase().trim();
    const password = req.body.password || '';
    const nextUrl = safeNext(req.body.next || '/admin');

    const installer = await db.one('SELECT id, password_hash FROM installers WHERE email = $1', [email]);
    // Constant-time comparison even on miss: run bcrypt against a dummy hash.
    const hash = installer && installer.password_hash
      ? installer.password_hash
      : '$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalidinv';
    const ok = await verifyPassword(password, hash);

    if (!installer || !ok) {
      return res.status(401).render('auth/login', {
        title: 'Installer login',
        error: GENERIC_LOGIN_ERROR,
        next: nextUrl
      });
    }

    await regenerateSession(req);
    req.session.installerId = installer.id;
    await db.query('UPDATE installers SET last_login_at = now() WHERE id = $1', [installer.id]);
    res.redirect(nextUrl);
  } catch (err) { next(err); }
});

router.get('/signup', (req, res) => {
  res.render('auth/signup', { title: 'Apply to list · National Paper Hangers', error: null, form: {} });
});

router.post('/signup', async (req, res, next) => {
  try {
    const f = req.body;
    const email = (f.email || '').toLowerCase().trim();
    const password = f.password || '';
    const businessName = (f.business_name || '').trim();
    const contactName = (f.contact_name || '').trim();
    const city = (f.city || '').trim();
    const stateAbbr = (f.state || '').trim().toUpperCase();
    const zip = (f.zip || '').trim();

    const errs = [];
    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');
    if (!stateAbbr || stateAbbr.length !== 2) errs.push('State (2-letter) required');

    if (errs.length) {
      return res.status(400).render('auth/signup', { title: 'Apply to list', error: errs.join('; '), form: f });
    }

    const exists = await db.one('SELECT id FROM installers WHERE email = $1', [email]);
    if (exists) {
      return res.status(400).render('auth/signup', { title: 'Apply to list', error: 'An account already exists for that email', form: f });
    }

    const baseSlug = slugify(businessName, { lower: true, strict: true }).slice(0, 60) || 'installer';
    let slug = baseSlug;
    let n = 2;
    while (await db.one('SELECT id FROM installers WHERE slug = $1', [slug])) {
      slug = `${baseSlug}-${n++}`;
    }

    const hash = await hashPassword(password);
    const r = await db.one(
      `INSERT INTO installers (slug, email, password_hash, business_name, contact_name, city, state, zip, status, tier, subscription_status)
       VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'pending','basic','inactive')
       RETURNING id`,
      [slug, email, hash, businessName, contactName, city, stateAbbr, zip]
    );

    await regenerateSession(req);
    req.session.installerId = r.id;
    res.redirect('/admin?welcome=1');
  } catch (err) { next(err); }
});

router.post('/logout', (req, res) => {
  req.session.destroy(() => res.redirect('/'));
});

module.exports = router;