← back to NationalPaperHangers

lib/auth.js

84 lines

const bcrypt = require('bcrypt');
const db = require('./db');

const SALT_ROUNDS = 12;

async function hashPassword(plain) {
  return bcrypt.hash(plain, SALT_ROUNDS);
}

async function verifyPassword(plain, hash) {
  if (!hash) return false;
  return bcrypt.compare(plain, hash);
}

// Explicit projection — NEVER include password_hash, claim_token, or
// claim_token_at in the row that gets attached to req.installer. If a view
// ever JSON.stringifies installer for debugging, this is the safety net.
const INSTALLER_SAFE_COLUMNS = `
  id, slug, email, business_name, contact_name, phone, bio, headline,
  city, state, zip, country, service_radius_miles, travel_available,
  team_size, founded_year, website, instagram_handle,
  market_segments, materials, brands_handled, accreditations,
  verified, verified_at, verified_by, insurance_on_file, insurance_expires,
  license_number, license_state,
  status, claim_status, claimed_at,
  source_name, source_url, source_scraped_at,
  tier, subscription_status, stripe_customer_id, stripe_subscription_id,
  current_period_end, response_time_hours, profile_complete,
  created_at, updated_at, last_login_at,
  ad_signals, ad_signals_at, avg_rating, review_count,
  equipment,
  latitude, longitude, geo_accuracy, geocoded_at,
  stripe_account_id, stripe_account_charges_enabled,
  stripe_account_payouts_enabled, stripe_account_onboarded_at
`;

async function loadInstaller(id) {
  if (!id) return null;
  return db.one(`SELECT ${INSTALLER_SAFE_COLUMNS} FROM installers WHERE id = $1`, [id]);
}

function requireInstaller(req, res, next) {
  if (!req.session || !req.session.installerId) {
    if (req.accepts('html')) return res.redirect('/login?next=' + encodeURIComponent(req.originalUrl));
    return res.status(401).json({ error: 'auth_required' });
  }
  next();
}

async function attachInstaller(req, res, next) {
  if (req.session && req.session.installerId) {
    try {
      req.installer = await loadInstaller(req.session.installerId);
    } catch (err) {
      console.error('[attachInstaller]', err.message);
    }
  }
  // Use a distinct local name so it doesn't collide with route-passed
  // `installer` (which is the profile-being-VIEWED on /installer/:slug,
  // not the logged-in user). Header partials read `currentInstaller`.
  res.locals.currentInstaller = req.installer || null;
  res.locals.installer = req.installer || null; // back-compat for admin views
  next();
}

function requirePaidTier(req, res, next) {
  const tier = req.installer && req.installer.tier;
  if (!req.installer) return res.redirect('/login');
  if (tier === 'pro' || tier === 'signature' || tier === 'enterprise') return next();
  if (req.accepts('html')) {
    return res.redirect('/admin/billing?upgrade=1');
  }
  return res.status(402).json({ error: 'upgrade_required' });
}

module.exports = {
  hashPassword,
  verifyPassword,
  loadInstaller,
  requireInstaller,
  attachInstaller,
  requirePaidTier
};