screentinker/server/lib/emailVerify.js
ScreenTinker b938fce368 feat(auth,tizen): TOTP 2FA UI, email verification on signup, Tizen SSSP install
Three features from this session, full server suite green (535/535).

TOTP 2FA (#100) — backend shipped without a UI; add it:
- Login: mfa_required -> 6-digit challenge (recovery codes accepted) -> /totp/verify.
- Settings > Account: enable (QR + confirm -> recovery codes once), regenerate,
  disable; SSO accounts see "managed by your identity provider".
- /totp/setup returns a server-rendered qr_data_url (bundled qrcode dep). keyuri
  folds the request Host into the issuer so multi-instance accounts are
  distinguishable in the authenticator app.

Email verification on signup — hosted HARD-block / self-host SOFT-nudge:
- email_verified column; existing users asked on first login (SSO + platform
  admins grandfathered); single-use 24h tokens (SHA-256 hashed).
- Gate engages only when email is configured (never locks out a no-mail instance).
  GET /verify-email + POST /resend-verification (generic, no account enumeration).
- Client: "confirm your email" flow + resend, verified/error toasts, self-host
  banner; onAuthSuccess refuses a tokenless response (defensive).

Tizen SSSP URL-Launcher install — Fusion-style one-URL native install:
- Server hosts /tizen/sssp_config.xml (dynamic <size>, always matches the served
  .wgt) + /tizen/ScreenTinker.wgt + a human landing. lib/wgt-cache.js resolves the
  signed .wgt (/data mount wins, mirroring the APK).
- build-wgt.sh also emits a static sssp_config.xml for CDN hosting.
- Retail panels require a Samsung Partner cert; dev-mode is SDB self-signed only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:08:50 -05:00

37 lines
1.7 KiB
JavaScript

'use strict';
// Signup email-verification tokens. The emailed token is random and stored ONLY as a
// SHA-256 hash (single-use, same discipline as recovery codes / api tokens) — the
// plaintext lives just in the email link. One pending token per user, kept on the users
// row (email_verify_hash + email_verify_expires), so a resend simply overwrites the old.
const crypto = require('crypto');
const { db } = require('../db/database');
const { hashToken } = require('../middleware/apiToken');
const TTL_SEC = 24 * 3600; // link valid for 24h
// Mint a fresh token for the user, store its hash + expiry, return the PLAINTEXT (emailed once).
function issue(userId) {
const token = crypto.randomBytes(32).toString('hex');
const expires = Math.floor(Date.now() / 1000) + TTL_SEC;
db.prepare('UPDATE users SET email_verify_hash = ?, email_verify_expires = ? WHERE id = ?')
.run(hashToken(token), expires, userId);
return token;
}
// Consume a token: mark the matching user verified + clear the token. Returns the user id on
// success, else null (unknown / expired / already-used). Single-use — the hash is cleared.
function consume(token) {
if (!token || typeof token !== 'string') return null;
const row = db.prepare('SELECT id, email_verify_expires FROM users WHERE email_verify_hash = ?')
.get(hashToken(token));
if (!row) return null;
if (!row.email_verify_expires || row.email_verify_expires < Math.floor(Date.now() / 1000)) return null;
db.prepare("UPDATE users SET email_verified = 1, email_verify_hash = NULL, email_verify_expires = NULL, updated_at = strftime('%s','now') WHERE id = ?")
.run(row.id);
return row.id;
}
module.exports = { issue, consume, TTL_SEC };