← 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
};