mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Replaces the unused EMAIL_WEBHOOK_URL stub with a real Microsoft Graph Mail.Send pipeline via @azure/msal-node client-credentials flow. Prior state on prod: every alert email was logged to journalctl and never sent (21 fallback log lines per hour for the chronic-offline devices). Four coordinated changes shipped as one commit since they're all part of making email delivery actually work responsibly: 1. services/email.js (NEW): Graph send via plain HTTPS (no SDK), in-memory MSAL token cache (refresh 60s pre-expiry), graceful stdout fallback when GRAPH_* env vars absent. Drop-in replacement for the old webhook. 2. services/alerts.js refactored: sequential await around sendEmail (was parallel fire-and-forget; first run hit Graph's MailboxConcurrency 429 ApplicationThrottled on a 30-device backlog). Sequential at ~250ms per send takes 5-8s for the full backlog, well within the 60s tick. Also: 24h long-offline cutoff to stop nagging about chronic-offline devices (the 20,000+ minute ones); 2-hour dedup window (was 1h) via a generic shouldSendAlert(type, id, windowMs) helper that future alert types (payment_failed, plan_limit_hit, etc.) can reuse. 3. Preferences UI: single checkbox in settings.js Account section bound to users.email_alerts. Saved via the existing Save Profile button. PUT /api/auth/me extended to accept email_alerts. requireAuth middleware SELECT now includes email_alerts so it propagates via req.user. 4. Dev safety net: GRAPH_DEV_RESTRICT_TO env var as an allow-list. When set, only listed recipients reach Graph; everyone else is suppressed with a log line. Prevents local dev (which often runs against fresh prod DB copies) from accidentally emailing real prod users. UNSET on prod systemd unit so production fans out normally. Also: package.json scripts use --env-file-if-exists=.env so local dev picks up .env automatically (Node 20.6+ built-in, no dotenv dep). Prod runs via systemd ExecStart and is unaffected. server/.gitignore added to keep .env out of git. Smoke verified end-to-end: - Sequential send pattern verified (a prior parallel-send tick had hit Graph's MailboxConcurrency 429 on 30 simultaneous sends; sequential at ~250ms each completes the same backlog without throttling) - 24h cutoff silenced 20/21 prod devices on the next tick - Dev restrict suppressed the 1 within-24h send - User-preference toggle flipped via UI -> DB -> alert path silently continued before reaching even the suppression log
56 lines
3 KiB
JavaScript
56 lines
3 KiB
JavaScript
const path = require('path');
|
|
|
|
module.exports = {
|
|
port: process.env.PORT || 3001,
|
|
httpsPort: process.env.HTTPS_PORT || 3443,
|
|
dbPath: path.join(__dirname, 'db', 'remote_display.db'),
|
|
uploadsDir: path.join(__dirname, 'uploads'),
|
|
contentDir: path.join(__dirname, 'uploads', 'content'),
|
|
screenshotsDir: path.join(__dirname, 'uploads', 'screenshots'),
|
|
frontendDir: path.join(__dirname, '..', 'frontend'),
|
|
heartbeatInterval: 10000, // Check every 10s
|
|
heartbeatTimeout: 45000, // Offline after 45s (3 missed 15s beats)
|
|
maxFileSize: 500 * 1024 * 1024, // 500MB
|
|
thumbnailWidth: 320,
|
|
screenshotQuality: 70,
|
|
// SSL: drop your Cloudflare Origin cert + key in certs/ folder
|
|
// or set env vars SSL_CERT and SSL_KEY to custom paths
|
|
sslCert: process.env.SSL_CERT || path.join(__dirname, 'certs', 'cert.pem'),
|
|
sslKey: process.env.SSL_KEY || path.join(__dirname, 'certs', 'key.pem'),
|
|
// Auth
|
|
jwtSecret: process.env.JWT_SECRET || (() => {
|
|
const secretFile = path.join(__dirname, 'certs', '.jwt_secret');
|
|
const fs = require('fs');
|
|
if (fs.existsSync(secretFile)) return fs.readFileSync(secretFile, 'utf8').trim();
|
|
const secret = require('crypto').randomBytes(64).toString('hex');
|
|
try { fs.mkdirSync(path.dirname(secretFile), { recursive: true }); fs.writeFileSync(secretFile, secret); } catch {}
|
|
return secret;
|
|
})(),
|
|
jwtExpiry: '7d',
|
|
// Google OAuth - set these in env or here
|
|
googleClientId: process.env.GOOGLE_CLIENT_ID || '',
|
|
// Microsoft OAuth - set these in env or here
|
|
microsoftClientId: process.env.MICROSOFT_CLIENT_ID || '',
|
|
microsoftTenantId: process.env.MICROSOFT_TENANT_ID || 'common',
|
|
// Stripe (optional - for paid subscriptions)
|
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY || '',
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
|
// Microsoft Graph email sender (services/email.js). Required for actual
|
|
// delivery; absent values short-circuit to a stdout fallback for local dev.
|
|
graphTenantId: process.env.GRAPH_TENANT_ID || '',
|
|
graphClientId: process.env.GRAPH_CLIENT_ID || '',
|
|
graphClientSecret: process.env.GRAPH_CLIENT_SECRET || '',
|
|
graphSenderEmail: process.env.GRAPH_SENDER_EMAIL || '',
|
|
graphSenderName: process.env.GRAPH_SENDER_NAME || 'ScreenTinker',
|
|
// Dev safety net: comma-separated allow-list of recipient emails. When set,
|
|
// sends to any address NOT in the list are suppressed (logged but not posted
|
|
// to Graph). Intended for local dev that pulls fresh prod DB copies - keeps
|
|
// us from accidentally emailing real prod users. UNSET on prod systemd unit.
|
|
graphDevRestrictTo: process.env.GRAPH_DEV_RESTRICT_TO || '',
|
|
// Self-hosted mode: if true, first user gets enterprise plan and no billing
|
|
selfHosted: process.env.SELF_HOSTED === 'true',
|
|
// Disable public registration (OAuth auto-signup is also blocked when set).
|
|
// First-user setup is still allowed so a fresh install can be initialized.
|
|
disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()),
|
|
};
|