web-agent-bridge 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.ar.md +1 -1
  2. package/README.md +336 -36
  3. package/docs/DEPLOY.md +118 -0
  4. package/docs/SPEC.md +1540 -0
  5. package/examples/mcp-agent.js +85 -0
  6. package/examples/vision-agent.js +12 -0
  7. package/package.json +14 -3
  8. package/public/admin/dashboard.html +848 -0
  9. package/public/admin/login.html +84 -0
  10. package/public/cookies.html +208 -0
  11. package/public/css/premium.css +317 -0
  12. package/public/dashboard.html +138 -0
  13. package/public/docs.html +5 -2
  14. package/public/index.html +54 -28
  15. package/public/js/auth-nav.js +31 -0
  16. package/public/js/auth-redirect.js +12 -0
  17. package/public/js/cookie-consent.js +56 -0
  18. package/public/js/ws-client.js +74 -0
  19. package/public/login.html +4 -2
  20. package/public/premium-dashboard.html +2075 -0
  21. package/public/premium.html +791 -0
  22. package/public/privacy.html +295 -0
  23. package/public/register.html +11 -2
  24. package/public/terms.html +254 -0
  25. package/script/ai-agent-bridge.js +253 -22
  26. package/sdk/index.js +36 -0
  27. package/server/config/secrets.js +92 -0
  28. package/server/index.js +100 -26
  29. package/server/middleware/adminAuth.js +30 -0
  30. package/server/middleware/auth.js +4 -7
  31. package/server/middleware/rateLimits.js +24 -0
  32. package/server/migrations/001_add_analytics_indexes.sql +7 -0
  33. package/server/migrations/002_premium_features.sql +418 -0
  34. package/server/models/db.js +360 -4
  35. package/server/routes/admin.js +247 -0
  36. package/server/routes/api.js +26 -9
  37. package/server/routes/billing.js +45 -0
  38. package/server/routes/discovery.js +324 -0
  39. package/server/routes/license.js +200 -11
  40. package/server/routes/noscript.js +543 -0
  41. package/server/routes/premium.js +724 -0
  42. package/server/services/email.js +204 -0
  43. package/server/services/fairness.js +420 -0
  44. package/server/services/premium.js +1680 -0
  45. package/server/services/stripe.js +192 -0
  46. package/server/utils/cache.js +125 -0
  47. package/server/utils/migrate.js +81 -0
  48. package/server/utils/secureFields.js +50 -0
  49. package/server/ws.js +33 -13
  50. package/wab-mcp-adapter/README.md +136 -0
  51. package/wab-mcp-adapter/index.js +528 -0
  52. package/wab-mcp-adapter/package.json +17 -0
@@ -1,8 +1,10 @@
1
1
  const Database = require('better-sqlite3');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
+ const crypto = require('crypto');
4
5
  const bcrypt = require('bcryptjs');
5
6
  const { v4: uuidv4 } = require('uuid');
7
+ const { encryptOptional, decryptOptional } = require('../utils/secureFields');
6
8
 
7
9
  const isTest = process.env.NODE_ENV === 'test';
8
10
  const DATA_DIR = isTest
@@ -71,6 +73,101 @@ db.exec(`
71
73
  CREATE INDEX IF NOT EXISTS idx_sites_license ON sites(license_key);
72
74
  CREATE INDEX IF NOT EXISTS idx_analytics_site ON analytics(site_id);
73
75
  CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created_at);
76
+
77
+ CREATE TABLE IF NOT EXISTS admins (
78
+ id TEXT PRIMARY KEY,
79
+ email TEXT UNIQUE NOT NULL,
80
+ password TEXT NOT NULL,
81
+ name TEXT NOT NULL,
82
+ role TEXT DEFAULT 'admin' CHECK(role IN ('admin','superadmin')),
83
+ created_at TEXT DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS free_grants (
87
+ id TEXT PRIMARY KEY,
88
+ user_id TEXT NOT NULL,
89
+ site_id TEXT,
90
+ granted_tier TEXT NOT NULL CHECK(granted_tier IN ('starter','pro','enterprise')),
91
+ reason TEXT,
92
+ granted_by TEXT NOT NULL,
93
+ granted_at TEXT DEFAULT (datetime('now')),
94
+ expires_at TEXT,
95
+ active INTEGER DEFAULT 1,
96
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
97
+ FOREIGN KEY (granted_by) REFERENCES admins(id)
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS stripe_customers (
101
+ id TEXT PRIMARY KEY,
102
+ user_id TEXT UNIQUE NOT NULL,
103
+ stripe_customer_id TEXT UNIQUE,
104
+ created_at TEXT DEFAULT (datetime('now')),
105
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS stripe_subscriptions (
109
+ id TEXT PRIMARY KEY,
110
+ user_id TEXT NOT NULL,
111
+ site_id TEXT NOT NULL,
112
+ stripe_subscription_id TEXT UNIQUE,
113
+ stripe_price_id TEXT,
114
+ tier TEXT NOT NULL CHECK(tier IN ('starter','pro','enterprise')),
115
+ status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','past_due','trialing','incomplete')),
116
+ current_period_start TEXT,
117
+ current_period_end TEXT,
118
+ created_at TEXT DEFAULT (datetime('now')),
119
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
120
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS payments (
124
+ id TEXT PRIMARY KEY,
125
+ user_id TEXT NOT NULL,
126
+ stripe_payment_id TEXT UNIQUE,
127
+ amount INTEGER NOT NULL,
128
+ currency TEXT DEFAULT 'usd',
129
+ status TEXT DEFAULT 'succeeded',
130
+ description TEXT,
131
+ created_at TEXT DEFAULT (datetime('now')),
132
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS notifications_log (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ user_id TEXT,
138
+ email_to TEXT NOT NULL,
139
+ template TEXT NOT NULL,
140
+ subject TEXT NOT NULL,
141
+ status TEXT DEFAULT 'sent' CHECK(status IN ('sent','failed','queued')),
142
+ error_message TEXT,
143
+ created_at TEXT DEFAULT (datetime('now'))
144
+ );
145
+
146
+ CREATE TABLE IF NOT EXISTS smtp_settings (
147
+ id INTEGER PRIMARY KEY CHECK(id = 1),
148
+ host TEXT,
149
+ port INTEGER DEFAULT 587,
150
+ secure INTEGER DEFAULT 0,
151
+ username TEXT,
152
+ password TEXT,
153
+ from_name TEXT DEFAULT 'Web Agent Bridge',
154
+ from_email TEXT,
155
+ enabled INTEGER DEFAULT 0,
156
+ updated_at TEXT DEFAULT (datetime('now'))
157
+ );
158
+
159
+ INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
160
+
161
+ CREATE TABLE IF NOT EXISTS platform_settings (
162
+ key TEXT PRIMARY KEY,
163
+ value TEXT,
164
+ updated_at TEXT DEFAULT (datetime('now'))
165
+ );
166
+
167
+ CREATE INDEX IF NOT EXISTS idx_free_grants_user ON free_grants(user_id);
168
+ CREATE INDEX IF NOT EXISTS idx_stripe_subs_user ON stripe_subscriptions(user_id);
169
+ CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
170
+ CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications_log(user_id);
74
171
  `);
75
172
 
76
173
  function generateLicenseKey() {
@@ -78,7 +175,8 @@ function generateLicenseKey() {
78
175
  const segments = [];
79
176
  for (let s = 0; s < 4; s++) {
80
177
  let seg = '';
81
- for (let i = 0; i < 5; i++) seg += chars[Math.floor(Math.random() * chars.length)];
178
+ const bytes = crypto.randomBytes(5);
179
+ for (let i = 0; i < 5; i++) seg += chars[bytes[i] % chars.length];
82
180
  segments.push(seg);
83
181
  }
84
182
  return `WAB-${segments.join('-')}`;
@@ -169,6 +267,10 @@ function verifyLicense(domain, licenseKey) {
169
267
  return { valid: false, error: 'Invalid license key', tier: 'free' };
170
268
  }
171
269
 
270
+ // Check for free grant override
271
+ const grant = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND (site_id = ? OR site_id IS NULL) AND active = 1 AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY granted_at DESC LIMIT 1`).get(site.user_id, site.id);
272
+ const effectiveTier = grant ? grant.granted_tier : site.tier;
273
+
172
274
  const tierPermissions = {
173
275
  free: { apiAccess: false, automatedLogin: false, extractData: false, advancedAnalytics: false },
174
276
  starter: { apiAccess: false, automatedLogin: true, extractData: false, advancedAnalytics: true },
@@ -178,17 +280,238 @@ function verifyLicense(domain, licenseKey) {
178
280
 
179
281
  return {
180
282
  valid: true,
181
- tier: site.tier,
283
+ tier: effectiveTier,
182
284
  domain: site.domain,
183
- allowedPermissions: tierPermissions[site.tier] || tierPermissions.free
285
+ allowedPermissions: tierPermissions[effectiveTier] || tierPermissions.free
184
286
  };
185
287
  }
186
288
 
289
+ // ─── Admin Operations ─────────────────────────────────────────────────
290
+ function createAdmin({ email, password, name, role }) {
291
+ const id = uuidv4();
292
+ const hashed = bcrypt.hashSync(password, 12);
293
+ db.prepare(`INSERT INTO admins (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`).run(id, email, hashed, name, role || 'admin');
294
+ return { id, email, name, role: role || 'admin' };
295
+ }
296
+
297
+ function loginAdmin({ email, password }) {
298
+ const admin = db.prepare(`SELECT * FROM admins WHERE email = ?`).get(email);
299
+ if (!admin) return null;
300
+ if (!bcrypt.compareSync(password, admin.password)) return null;
301
+ return { id: admin.id, email: admin.email, name: admin.name, role: admin.role };
302
+ }
303
+
304
+ function findAdminById(id) {
305
+ return db.prepare(`SELECT id, email, name, role, created_at FROM admins WHERE id = ?`).get(id);
306
+ }
307
+
308
+ /**
309
+ * First-run admin creation from env only (no hardcoded password).
310
+ * Alternatively use: node scripts/create-admin.js <email> <password>
311
+ */
312
+ function maybeBootstrapAdmin() {
313
+ if (isTest) return;
314
+ const count = db.prepare(`SELECT COUNT(*) as c FROM admins`).get().c;
315
+ if (count > 0) return;
316
+ const email = process.env.BOOTSTRAP_ADMIN_EMAIL;
317
+ const password = process.env.BOOTSTRAP_ADMIN_PASSWORD;
318
+ if (!email || !password) {
319
+ console.warn('[WAB] No admin accounts. Set BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD for first boot, or run: node scripts/create-admin.js <email> <password>');
320
+ return;
321
+ }
322
+ createAdmin({ email, password, name: 'Bootstrap Admin', role: 'superadmin' });
323
+ console.log('[WAB] Bootstrap admin created from BOOTSTRAP_ADMIN_* environment variables.');
324
+ }
325
+
326
+ // ─── Admin Queries ────────────────────────────────────────────────────
327
+ function getAllUsers() {
328
+ return db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC`).all();
329
+ }
330
+
331
+ function getAllSites() {
332
+ return db.prepare(`SELECT s.*, u.email as user_email, u.name as user_name FROM sites s LEFT JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC`).all();
333
+ }
334
+
335
+ function getAdminStats() {
336
+ const totalUsers = db.prepare(`SELECT COUNT(*) as c FROM users`).get().c;
337
+ const totalSites = db.prepare(`SELECT COUNT(*) as c FROM sites WHERE active = 1`).get().c;
338
+ const totalAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics`).get().c;
339
+ const todayAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics WHERE created_at >= date('now')`).get().c;
340
+ const tierBreakdown = db.prepare(`SELECT tier, COUNT(*) as count FROM sites WHERE active = 1 GROUP BY tier`).all();
341
+ const recentUsers = db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC LIMIT 10`).all();
342
+ const totalRevenue = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE status = 'succeeded'`).get().total;
343
+ const activeGrants = db.prepare(`SELECT COUNT(*) as c FROM free_grants WHERE active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))`).get().c;
344
+ const monthlySignups = db.prepare(`SELECT COUNT(*) as c FROM users WHERE created_at >= date('now', '-30 days')`).get().c;
345
+ return { totalUsers, totalSites, totalAnalytics, todayAnalytics, tierBreakdown, recentUsers, totalRevenue, activeGrants, monthlySignups };
346
+ }
347
+
348
+ function getPlatformAnalytics(days) {
349
+ const since = new Date(Date.now() - days * 86400000).toISOString();
350
+ const timeline = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
351
+ const topActions = db.prepare(`SELECT action_name, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY action_name ORDER BY count DESC LIMIT 20`).all(since);
352
+ const signups = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM users WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
353
+ return { timeline, topActions, signups };
354
+ }
355
+
356
+ // ─── Free Grant Operations ────────────────────────────────────────────
357
+ function grantFreeTier({ userId, siteId, tier, reason, grantedBy, expiresAt }) {
358
+ const id = uuidv4();
359
+ db.prepare(`INSERT INTO free_grants (id, user_id, site_id, granted_tier, reason, granted_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId || null, tier, reason || null, grantedBy, expiresAt || null);
360
+ if (siteId) {
361
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, siteId);
362
+ } else {
363
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
364
+ }
365
+ return { id, userId, siteId, tier, reason };
366
+ }
367
+
368
+ function revokeGrant(grantId) {
369
+ const grant = db.prepare(`SELECT * FROM free_grants WHERE id = ?`).get(grantId);
370
+ if (!grant) return false;
371
+ db.prepare(`UPDATE free_grants SET active = 0 WHERE id = ?`).run(grantId);
372
+ if (grant.site_id) {
373
+ db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE id = ?`).run(grant.site_id);
374
+ } else {
375
+ db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(grant.user_id);
376
+ }
377
+ return true;
378
+ }
379
+
380
+ function getActiveGrants() {
381
+ return db.prepare(`SELECT g.*, u.email as user_email, u.name as user_name, a.name as admin_name FROM free_grants g LEFT JOIN users u ON g.user_id = u.id LEFT JOIN admins a ON g.granted_by = a.id WHERE g.active = 1 ORDER BY g.granted_at DESC`).all();
382
+ }
383
+
384
+ // ─── Stripe DB Operations ─────────────────────────────────────────────
385
+ function saveStripeCustomer(userId, stripeCustomerId) {
386
+ const id = uuidv4();
387
+ db.prepare(`INSERT OR REPLACE INTO stripe_customers (id, user_id, stripe_customer_id) VALUES (?, ?, ?)`).run(id, userId, stripeCustomerId);
388
+ }
389
+
390
+ function getStripeCustomer(userId) {
391
+ return db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
392
+ }
393
+
394
+ function saveStripeSubscription({ userId, siteId, stripeSubId, stripePriceId, tier, status, periodStart, periodEnd }) {
395
+ const id = uuidv4();
396
+ db.prepare(`INSERT INTO stripe_subscriptions (id, user_id, site_id, stripe_subscription_id, stripe_price_id, tier, status, current_period_start, current_period_end) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId, stripeSubId, stripePriceId, tier, status || 'active', periodStart, periodEnd);
397
+ }
398
+
399
+ function updateStripeSubscription(stripeSubId, { status, periodStart, periodEnd, tier }) {
400
+ const updates = [];
401
+ const params = [];
402
+ if (status) { updates.push('status = ?'); params.push(status); }
403
+ if (periodStart) { updates.push('current_period_start = ?'); params.push(periodStart); }
404
+ if (periodEnd) { updates.push('current_period_end = ?'); params.push(periodEnd); }
405
+ if (tier) { updates.push('tier = ?'); params.push(tier); }
406
+ if (updates.length === 0) return;
407
+ params.push(stripeSubId);
408
+ db.prepare(`UPDATE stripe_subscriptions SET ${updates.join(', ')} WHERE stripe_subscription_id = ?`).run(...params);
409
+ }
410
+
411
+ function getStripeSubscriptionBySubId(stripeSubId) {
412
+ return db.prepare(`SELECT * FROM stripe_subscriptions WHERE stripe_subscription_id = ?`).get(stripeSubId);
413
+ }
414
+
415
+ function savePayment({ userId, stripePaymentId, amount, currency, status, description }) {
416
+ const id = uuidv4();
417
+ db.prepare(`INSERT INTO payments (id, user_id, stripe_payment_id, amount, currency, status, description) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, stripePaymentId, amount, currency || 'usd', status || 'succeeded', description || null);
418
+ }
419
+
420
+ function getPayments(limit) {
421
+ return db.prepare(`SELECT p.*, u.email as user_email, u.name as user_name FROM payments p LEFT JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT ?`).all(limit || 50);
422
+ }
423
+
424
+ // ─── SMTP Settings ────────────────────────────────────────────────────
425
+ function getSmtpSettings() {
426
+ const row = db.prepare(`SELECT * FROM smtp_settings WHERE id = 1`).get();
427
+ if (!row) return null;
428
+ if (row.password) {
429
+ const dec = decryptOptional(row.password);
430
+ return { ...row, password: dec != null ? dec : row.password };
431
+ }
432
+ return row;
433
+ }
434
+
435
+ function updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled }) {
436
+ const current = db.prepare(`SELECT password FROM smtp_settings WHERE id = 1`).get();
437
+ let nextPassword = current && current.password;
438
+ if (password !== undefined) {
439
+ nextPassword = encryptOptional(password);
440
+ }
441
+ db.prepare(`UPDATE smtp_settings SET host = ?, port = ?, secure = ?, username = ?, password = ?, from_name = ?, from_email = ?, enabled = ?, updated_at = datetime('now') WHERE id = 1`).run(host, port || 587, secure ? 1 : 0, username, nextPassword, fromName || 'Web Agent Bridge', fromEmail, enabled ? 1 : 0);
442
+ }
443
+
444
+ function logNotification({ userId, emailTo, template, subject, status, errorMessage }) {
445
+ db.prepare(`INSERT INTO notifications_log (user_id, email_to, template, subject, status, error_message) VALUES (?, ?, ?, ?, ?, ?)`).run(userId || null, emailTo, template, subject, status || 'sent', errorMessage || null);
446
+ }
447
+
448
+ function getNotificationLogs(limit) {
449
+ return db.prepare(`SELECT * FROM notifications_log ORDER BY created_at DESC LIMIT ?`).all(limit || 100);
450
+ }
451
+
452
+ // ─── Admin User Management ───────────────────────────────────────────
453
+ function adminUpdateUserTier(userId, siteId, tier) {
454
+ if (siteId) {
455
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`).run(tier, siteId, userId);
456
+ } else {
457
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Admin: update any site by id (tier and/or active).
463
+ *
464
+ * @param {string} siteId Site UUID.
465
+ * @param {{ tier?: string, active?: boolean }} updates Partial updates.
466
+ * @returns {boolean}
467
+ */
468
+ function adminUpdateSite(siteId, updates) {
469
+ const site = findSiteById.get(siteId);
470
+ if (!site) return false;
471
+ let tier = site.tier;
472
+ let active = site.active;
473
+ if (updates.tier !== undefined) {
474
+ if (!['free', 'starter', 'pro', 'enterprise'].includes(updates.tier)) return false;
475
+ tier = updates.tier;
476
+ }
477
+ if (updates.active !== undefined) {
478
+ active = updates.active ? 1 : 0;
479
+ }
480
+ db.prepare(`UPDATE sites SET tier = ?, active = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, active, siteId);
481
+ return true;
482
+ }
483
+
484
+ function adminDeleteUser(userId) {
485
+ db.prepare(`UPDATE sites SET active = 0 WHERE user_id = ?`).run(userId);
486
+ db.prepare(`DELETE FROM users WHERE id = ?`).run(userId);
487
+ }
488
+
489
+ function getUserFullDetails(userId) {
490
+ const user = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`).get(userId);
491
+ if (!user) return null;
492
+ const sites = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
493
+ const grants = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND active = 1`).all(userId);
494
+ const payments = db.prepare(`SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
495
+ const stripeCustomer = db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
496
+ return { ...user, sites, grants, payments, stripeCustomer };
497
+ }
498
+
499
+ // ─── Platform Settings ───────────────────────────────────────────────
500
+ function getPlatformSetting(key) {
501
+ const row = db.prepare(`SELECT value FROM platform_settings WHERE key = ?`).get(key);
502
+ return row ? row.value : null;
503
+ }
504
+
505
+ function setPlatformSetting(key, value) {
506
+ db.prepare(`INSERT OR REPLACE INTO platform_settings (key, value, updated_at) VALUES (?, ?, datetime('now'))`).run(key, value);
507
+ }
508
+
187
509
  module.exports = {
188
510
  db,
189
511
  registerUser,
190
512
  loginUser,
191
513
  findUserById,
514
+ findUserByEmail,
192
515
  addSite,
193
516
  findSitesByUser,
194
517
  findSiteById,
@@ -201,5 +524,38 @@ module.exports = {
201
524
  getAnalyticsTimeline,
202
525
  verifyLicense,
203
526
  generateLicenseKey,
204
- generateApiKey
527
+ generateApiKey,
528
+ // Admin
529
+ createAdmin,
530
+ loginAdmin,
531
+ findAdminById,
532
+ maybeBootstrapAdmin,
533
+ getAllUsers,
534
+ getAllSites,
535
+ getAdminStats,
536
+ getPlatformAnalytics,
537
+ adminUpdateUserTier,
538
+ adminUpdateSite,
539
+ adminDeleteUser,
540
+ getUserFullDetails,
541
+ // Free Grants
542
+ grantFreeTier,
543
+ revokeGrant,
544
+ getActiveGrants,
545
+ // Stripe
546
+ saveStripeCustomer,
547
+ getStripeCustomer,
548
+ saveStripeSubscription,
549
+ updateStripeSubscription,
550
+ getStripeSubscriptionBySubId,
551
+ savePayment,
552
+ getPayments,
553
+ // SMTP
554
+ getSmtpSettings,
555
+ updateSmtpSettings,
556
+ logNotification,
557
+ getNotificationLogs,
558
+ // Platform
559
+ getPlatformSetting,
560
+ setPlatformSetting
205
561
  };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Admin API Routes
3
+ * Full admin panel backend: users, sites, analytics, Stripe, SMTP, grants
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const { authenticateAdmin, generateAdminToken } = require('../middleware/adminAuth');
9
+ const {
10
+ loginAdmin, findAdminById, createAdmin,
11
+ getAllUsers, getAllSites, getAdminStats, getPlatformAnalytics,
12
+ getUserFullDetails, adminUpdateUserTier, adminUpdateSite, adminDeleteUser,
13
+ grantFreeTier, revokeGrant, getActiveGrants,
14
+ getSmtpSettings, updateSmtpSettings, getNotificationLogs,
15
+ getPayments, getPlatformSetting, setPlatformSetting,
16
+ findUserByEmail,
17
+ findSiteById,
18
+ getAnalyticsBySite,
19
+ getAnalyticsTimeline
20
+ } = require('../models/db');
21
+ const { sendEmail } = require('../services/email');
22
+ const { createCheckoutSession, createPortalSession, isStripeConfigured, getStripePrices } = require('../services/stripe');
23
+
24
+ // ─── Auth ──────────────────────────────────────────────────────────────
25
+
26
+ router.post('/login', (req, res) => {
27
+ const { email, password } = req.body;
28
+ if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
29
+
30
+ const admin = loginAdmin({ email, password });
31
+ if (!admin) return res.status(401).json({ error: 'Invalid credentials' });
32
+
33
+ const token = generateAdminToken(admin);
34
+ res.json({ admin, token });
35
+ });
36
+
37
+ router.get('/me', authenticateAdmin, (req, res) => {
38
+ const admin = findAdminById(req.admin.id);
39
+ if (!admin) return res.status(404).json({ error: 'Admin not found' });
40
+ res.json({ admin });
41
+ });
42
+
43
+ // ─── Dashboard Stats ──────────────────────────────────────────────────
44
+
45
+ router.get('/stats', authenticateAdmin, (req, res) => {
46
+ const stats = getAdminStats();
47
+ stats.stripeConfigured = isStripeConfigured();
48
+ res.json(stats);
49
+ });
50
+
51
+ router.get('/analytics', authenticateAdmin, (req, res) => {
52
+ const days = parseInt(req.query.days) || 30;
53
+ const data = getPlatformAnalytics(days);
54
+ res.json(data);
55
+ });
56
+
57
+ // ─── Users Management ─────────────────────────────────────────────────
58
+
59
+ router.get('/users', authenticateAdmin, (req, res) => {
60
+ const users = getAllUsers();
61
+ res.json({ users });
62
+ });
63
+
64
+ router.get('/users/:id', authenticateAdmin, (req, res) => {
65
+ const user = getUserFullDetails(req.params.id);
66
+ if (!user) return res.status(404).json({ error: 'User not found' });
67
+ res.json({ user });
68
+ });
69
+
70
+ router.put('/users/:id/tier', authenticateAdmin, (req, res) => {
71
+ const { tier, siteId } = req.body;
72
+ if (!['free', 'starter', 'pro', 'enterprise'].includes(tier)) {
73
+ return res.status(400).json({ error: 'Invalid tier' });
74
+ }
75
+ adminUpdateUserTier(req.params.id, siteId, tier);
76
+ res.json({ success: true });
77
+ });
78
+
79
+ router.delete('/users/:id', authenticateAdmin, (req, res) => {
80
+ adminDeleteUser(req.params.id);
81
+ res.json({ success: true });
82
+ });
83
+
84
+ // ─── Sites Management ─────────────────────────────────────────────────
85
+
86
+ router.get('/sites', authenticateAdmin, (req, res) => {
87
+ const sites = getAllSites();
88
+ res.json({ sites });
89
+ });
90
+
91
+ router.put('/sites/:id', authenticateAdmin, (req, res) => {
92
+ const { tier, active } = req.body;
93
+ const ok = adminUpdateSite(req.params.id, { tier, active });
94
+ if (!ok) return res.status(404).json({ error: 'Site not found or invalid tier' });
95
+ res.json({ success: true });
96
+ });
97
+
98
+ router.get('/sites/:id/analytics', authenticateAdmin, (req, res) => {
99
+ const site = findSiteById.get(req.params.id);
100
+ if (!site) return res.status(404).json({ error: 'Site not found' });
101
+ const days = parseInt(req.query.days, 10) || 30;
102
+ const since = new Date(Date.now() - days * 86400000).toISOString();
103
+ const summary = getAnalyticsBySite.all(site.id, since);
104
+ const timeline = getAnalyticsTimeline.all(site.id, since);
105
+ res.json({
106
+ site: {
107
+ id: site.id,
108
+ name: site.name,
109
+ domain: site.domain,
110
+ tier: site.tier,
111
+ license_key: site.license_key
112
+ },
113
+ summary,
114
+ timeline,
115
+ period: `${days} days`
116
+ });
117
+ });
118
+
119
+ // ─── Free Grants ──────────────────────────────────────────────────────
120
+
121
+ router.get('/grants', authenticateAdmin, (req, res) => {
122
+ const grants = getActiveGrants();
123
+ res.json({ grants });
124
+ });
125
+
126
+ router.post('/grants', authenticateAdmin, (req, res) => {
127
+ const { userId, siteId, tier, reason, expiresAt } = req.body;
128
+ if (!userId || !tier) return res.status(400).json({ error: 'userId and tier required' });
129
+ if (!['starter', 'pro', 'enterprise'].includes(tier)) return res.status(400).json({ error: 'Invalid tier' });
130
+
131
+ const grant = grantFreeTier({ userId, siteId, tier, reason, grantedBy: req.admin.id, expiresAt });
132
+
133
+ // Send notification email
134
+ const user = getUserFullDetails(userId);
135
+ if (user) {
136
+ sendEmail({
137
+ to: user.email,
138
+ template: 'tier_upgrade',
139
+ data: { name: user.name, tier, reason: reason || 'Complimentary upgrade' },
140
+ userId
141
+ }).catch(() => {});
142
+ }
143
+
144
+ res.status(201).json({ grant });
145
+ });
146
+
147
+ router.delete('/grants/:id', authenticateAdmin, (req, res) => {
148
+ const ok = revokeGrant(req.params.id);
149
+ if (!ok) return res.status(404).json({ error: 'Grant not found' });
150
+ res.json({ success: true });
151
+ });
152
+
153
+ // ─── Stripe Settings ─────────────────────────────────────────────────
154
+
155
+ router.get('/stripe/config', authenticateAdmin, (req, res) => {
156
+ const secretKey = getPlatformSetting('stripe_secret_key');
157
+ const publishableKey = getPlatformSetting('stripe_publishable_key');
158
+ const webhookSecret = getPlatformSetting('stripe_webhook_secret');
159
+ const prices = getStripePrices();
160
+
161
+ res.json({
162
+ configured: isStripeConfigured(),
163
+ hasSecretKey: !!secretKey,
164
+ publishableKey: publishableKey || '',
165
+ webhookSecret: webhookSecret ? '••••' + webhookSecret.slice(-4) : '',
166
+ prices
167
+ });
168
+ });
169
+
170
+ router.put('/stripe/config', authenticateAdmin, (req, res) => {
171
+ const { secretKey, publishableKey, webhookSecret, priceStarter, pricePro, priceEnterprise } = req.body;
172
+
173
+ if (secretKey) setPlatformSetting('stripe_secret_key', secretKey);
174
+ if (publishableKey) setPlatformSetting('stripe_publishable_key', publishableKey);
175
+ if (webhookSecret) setPlatformSetting('stripe_webhook_secret', webhookSecret);
176
+ if (priceStarter) setPlatformSetting('stripe_price_starter', priceStarter);
177
+ if (pricePro) setPlatformSetting('stripe_price_pro', pricePro);
178
+ if (priceEnterprise) setPlatformSetting('stripe_price_enterprise', priceEnterprise);
179
+
180
+ res.json({ success: true });
181
+ });
182
+
183
+ // ─── Payments ──────────────────────────────────────────────────────────
184
+
185
+ router.get('/payments', authenticateAdmin, (req, res) => {
186
+ const limit = parseInt(req.query.limit) || 50;
187
+ const payments = getPayments(limit);
188
+ res.json({ payments });
189
+ });
190
+
191
+ // ─── SMTP Settings ────────────────────────────────────────────────────
192
+
193
+ router.get('/smtp', authenticateAdmin, (req, res) => {
194
+ const settings = getSmtpSettings();
195
+ // Mask password
196
+ if (settings && settings.password) {
197
+ settings.password = '••••••••';
198
+ }
199
+ res.json({ settings });
200
+ });
201
+
202
+ router.put('/smtp', authenticateAdmin, (req, res) => {
203
+ const { host, port, secure, username, password, fromName, fromEmail, enabled } = req.body;
204
+ if (!host || !username || !fromEmail) {
205
+ return res.status(400).json({ error: 'Host, username, and fromEmail are required' });
206
+ }
207
+ updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled });
208
+ res.json({ success: true });
209
+ });
210
+
211
+ router.post('/smtp/test', authenticateAdmin, (req, res) => {
212
+ const { testEmail } = req.body;
213
+ if (!testEmail) return res.status(400).json({ error: 'testEmail required' });
214
+
215
+ sendEmail({
216
+ to: testEmail,
217
+ template: 'welcome',
218
+ data: { name: 'Test User', dashboardUrl: 'https://webagentbridge.com/dashboard' }
219
+ }).then(result => {
220
+ res.json(result);
221
+ }).catch(err => {
222
+ res.status(500).json({ success: false, error: err.message });
223
+ });
224
+ });
225
+
226
+ // ─── Notification Logs ────────────────────────────────────────────────
227
+
228
+ router.get('/notifications', authenticateAdmin, (req, res) => {
229
+ const limit = parseInt(req.query.limit) || 100;
230
+ const logs = getNotificationLogs(limit);
231
+ res.json({ logs });
232
+ });
233
+
234
+ // ─── Send Custom Notification ─────────────────────────────────────────
235
+
236
+ router.post('/notifications/send', authenticateAdmin, (req, res) => {
237
+ const { userId, email, template, data } = req.body;
238
+ if (!email || !template) return res.status(400).json({ error: 'email and template required' });
239
+
240
+ sendEmail({ to: email, template, data: data || {}, userId }).then(result => {
241
+ res.json(result);
242
+ }).catch(err => {
243
+ res.status(500).json({ success: false, error: err.message });
244
+ });
245
+ });
246
+
247
+ module.exports = router;