printhive/lib/auth.js
dlawler489 f3a69c82ec Open models in your slicer and drag files out
- Settings → Slicer: pick Bambu Studio, OrcaSlicer, PrusaSlicer, Cura, a
  custom open-link template, or plain download (remembered per browser)
- Bambu Studio / OrcaSlicer launch via their URL schemes with a 10-minute
  signed download link (/dl/<token>/<name>) that needs no session cookie
- PrusaSlicer and Cura fall back to a download (they can't open our links)
- 3MF projects report which app saved them; opening one in a different
  slicer asks first
- "slicer" action on every file row and archive entry
- Drag model cards (file, or zip of all files) and file/entry rows out to
  the desktop via Chromium's DownloadURL

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:15:37 +10:00

78 lines
2.2 KiB
JavaScript

const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const store = require('./store');
const COOKIE = 'ph_session';
const TOKEN_TTL = '30d';
// JWT signing secret — generated once and persisted in settings.
let cachedSecret = null;
async function getSecret() {
if (cachedSecret) return cachedSecret;
let secret = await store.getSetting('auth_secret');
if (!secret) {
secret = crypto.randomBytes(48).toString('hex');
await store.setSetting('auth_secret', secret);
}
cachedSecret = secret;
return secret;
}
function hashPassword(password) {
return bcrypt.hash(password, 12);
}
function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
async function issueToken(user) {
return jwt.sign({ sub: user.id, email: user.email }, await getSecret(), { expiresIn: TOKEN_TTL });
}
function setAuthCookie(res, token) {
res.cookie(COOKIE, token, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.COOKIE_SECURE === 'true',
maxAge: 30 * 24 * 60 * 60 * 1000,
});
}
function clearAuthCookie(res) {
res.clearCookie(COOKIE);
}
// Resolve the current user from the session cookie, or null.
async function currentUser(req) {
const token = req.cookies && req.cookies[COOKIE];
if (!token) return null;
try {
const payload = jwt.verify(token, await getSecret());
return store.getUserById(payload.sub);
} catch {
return null;
}
}
// Express middleware: 401 unless authenticated.
async function requireAuth(req, res, next) {
const user = await currentUser(req);
if (!user) return res.status(401).json({ error: 'Authentication required' });
req.user = user;
next();
}
// Password-reset tokens: return { token, hash } — store the hash, email the token.
function makeResetToken() {
const token = crypto.randomBytes(32).toString('hex');
const hash = crypto.createHash('sha256').update(token).digest('hex');
return { token, hash };
}
function hashResetToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
module.exports = {
COOKIE, getSecret, hashPassword, verifyPassword, issueToken, setAuthCookie, clearAuthCookie,
currentUser, requireAuth, makeResetToken, hashResetToken,
};