@sonicjs-cms/core 2.14.0 → 2.16.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.
- package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +17 -0
- package/dist/admin-layout-catalyst.template-HFD37TY5.cjs.map +1 -0
- package/dist/admin-layout-catalyst.template-UMTIN66R.js +7 -0
- package/dist/admin-layout-catalyst.template-UMTIN66R.js.map +1 -0
- package/dist/{chunk-3QCEYJLK.cjs → chunk-4ZSNJDLS.cjs} +9 -9
- package/dist/{chunk-3QCEYJLK.cjs.map → chunk-4ZSNJDLS.cjs.map} +1 -1
- package/dist/chunk-55RDMDOP.js +684 -0
- package/dist/chunk-55RDMDOP.js.map +1 -0
- package/dist/{chunk-6FHNRRJ3.cjs → chunk-635JAMSE.cjs} +76 -17
- package/dist/chunk-635JAMSE.cjs.map +1 -0
- package/dist/{chunk-DRWSHIFG.cjs → chunk-6ENX7QSA.cjs} +228 -658
- package/dist/chunk-6ENX7QSA.cjs.map +1 -0
- package/dist/{chunk-56GUBLJE.cjs → chunk-ABB34XUS.cjs} +13 -13
- package/dist/{chunk-56GUBLJE.cjs.map → chunk-ABB34XUS.cjs.map} +1 -1
- package/dist/{chunk-YYMPHM3I.cjs → chunk-CZ6BVQZX.cjs} +19 -9
- package/dist/chunk-CZ6BVQZX.cjs.map +1 -0
- package/dist/{chunk-J5WGMRSU.js → chunk-EXNEW5US.js} +76 -17
- package/dist/chunk-EXNEW5US.js.map +1 -0
- package/dist/{chunk-H3XXBAMO.js → chunk-INSDRCG3.js} +722 -212
- package/dist/chunk-INSDRCG3.js.map +1 -0
- package/dist/{chunk-GAVTTYMC.js → chunk-MVSCB4E3.js} +3 -3
- package/dist/{chunk-GAVTTYMC.js.map → chunk-MVSCB4E3.js.map} +1 -1
- package/dist/{chunk-KZ2MFGET.cjs → chunk-OCLUXJ7E.cjs} +9 -2
- package/dist/chunk-OCLUXJ7E.cjs.map +1 -0
- package/dist/{chunk-QP3OHHON.cjs → chunk-OHYBNCVL.cjs} +18 -696
- package/dist/chunk-OHYBNCVL.cjs.map +1 -0
- package/dist/{chunk-CB7ONLGB.js → chunk-ON5ZMSU4.js} +3 -3
- package/dist/{chunk-CB7ONLGB.js.map → chunk-ON5ZMSU4.js.map} +1 -1
- package/dist/{chunk-I6FFGQIT.cjs → chunk-Q5VFZUXV.cjs} +723 -211
- package/dist/chunk-Q5VFZUXV.cjs.map +1 -0
- package/dist/{chunk-2MXF4RYZ.js → chunk-TFNTM3OA.js} +3 -3
- package/dist/{chunk-2MXF4RYZ.js.map → chunk-TFNTM3OA.js.map} +1 -1
- package/dist/chunk-UYJ6TJHX.cjs +691 -0
- package/dist/chunk-UYJ6TJHX.cjs.map +1 -0
- package/dist/{chunk-JKNKO6LA.js → chunk-VFQUULAV.js} +9 -2
- package/dist/chunk-VFQUULAV.js.map +1 -0
- package/dist/{chunk-23DP6TO5.js → chunk-WLSIUKNM.js} +44 -474
- package/dist/chunk-WLSIUKNM.js.map +1 -0
- package/dist/{chunk-JTUCC6WZ.js → chunk-XWIA3HVX.js} +9 -683
- package/dist/chunk-XWIA3HVX.js.map +1 -0
- package/dist/{chunk-AFGOH2F6.js → chunk-Y5EH32F5.js} +15 -5
- package/dist/chunk-Y5EH32F5.js.map +1 -0
- package/dist/{chunk-YULUPQZV.cjs → chunk-YQW2GCJ3.cjs} +3 -3
- package/dist/{chunk-YULUPQZV.cjs.map → chunk-YQW2GCJ3.cjs.map} +1 -1
- package/dist/index.cjs +1591 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1464 -109
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +29 -29
- package/dist/middleware.js +3 -3
- package/dist/migrations-7HQ7LYAL.js +4 -0
- package/dist/{migrations-WKONKRN7.js.map → migrations-7HQ7LYAL.js.map} +1 -1
- package/dist/migrations-SVQTT7NV.cjs +13 -0
- package/dist/{migrations-F7KVA74T.cjs.map → migrations-SVQTT7NV.cjs.map} +1 -1
- package/dist/{plugin-bootstrap-BGwBraaN.d.cts → plugin-bootstrap-DfVerYV4.d.cts} +2 -1
- package/dist/{plugin-bootstrap-Drns7X9w.d.ts → plugin-bootstrap-P_ciLp_C.d.ts} +2 -1
- package/dist/plugins.cjs +11 -11
- package/dist/plugins.js +2 -2
- package/dist/routes.cjs +31 -30
- package/dist/routes.js +8 -7
- package/dist/services.cjs +23 -23
- package/dist/services.d.cts +1 -1
- package/dist/services.d.ts +1 -1
- package/dist/services.js +2 -2
- package/dist/templates.cjs +26 -25
- package/dist/templates.js +3 -2
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/migrations/036_analytics_events.sql +22 -0
- package/package.json +1 -1
- package/dist/chunk-23DP6TO5.js.map +0 -1
- package/dist/chunk-6FHNRRJ3.cjs.map +0 -1
- package/dist/chunk-AFGOH2F6.js.map +0 -1
- package/dist/chunk-DRWSHIFG.cjs.map +0 -1
- package/dist/chunk-H3XXBAMO.js.map +0 -1
- package/dist/chunk-I6FFGQIT.cjs.map +0 -1
- package/dist/chunk-J5WGMRSU.js.map +0 -1
- package/dist/chunk-JKNKO6LA.js.map +0 -1
- package/dist/chunk-JTUCC6WZ.js.map +0 -1
- package/dist/chunk-KZ2MFGET.cjs.map +0 -1
- package/dist/chunk-QP3OHHON.cjs.map +0 -1
- package/dist/chunk-YYMPHM3I.cjs.map +0 -1
- package/dist/migrations-F7KVA74T.cjs +0 -13
- package/dist/migrations-WKONKRN7.js +0 -4
package/dist/index.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-
|
|
2
|
-
export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-
|
|
1
|
+
import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-WLSIUKNM.js';
|
|
2
|
+
export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-WLSIUKNM.js';
|
|
3
3
|
import { SettingsService, setAppInstance, schema_exports } from './chunk-TBJY2FF7.js';
|
|
4
4
|
export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-TBJY2FF7.js';
|
|
5
|
-
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-
|
|
6
|
-
export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-
|
|
7
|
-
import { PluginService } from './chunk-
|
|
8
|
-
export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-
|
|
9
|
-
export { MigrationService } from './chunk-
|
|
10
|
-
export { renderFilterBar } from './chunk-
|
|
11
|
-
import {
|
|
12
|
-
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-Y5EH32F5.js';
|
|
6
|
+
export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-Y5EH32F5.js';
|
|
7
|
+
import { PluginService, PLUGIN_REGISTRY } from './chunk-INSDRCG3.js';
|
|
8
|
+
export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-INSDRCG3.js';
|
|
9
|
+
export { MigrationService } from './chunk-VFQUULAV.js';
|
|
10
|
+
export { renderFilterBar } from './chunk-ON5ZMSU4.js';
|
|
11
|
+
import { renderAdminLayout } from './chunk-XWIA3HVX.js';
|
|
12
|
+
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-XWIA3HVX.js';
|
|
13
|
+
import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-55RDMDOP.js';
|
|
14
|
+
export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-TFNTM3OA.js';
|
|
15
|
+
import { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
|
|
16
|
+
export { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
|
|
17
|
+
import { package_default, getCoreVersion } from './chunk-MVSCB4E3.js';
|
|
18
|
+
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-MVSCB4E3.js';
|
|
18
19
|
import './chunk-X7ZAEI5S.js';
|
|
19
20
|
export { metricsTracker } from './chunk-FICTAGD4.js';
|
|
20
21
|
export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
|
|
@@ -5704,7 +5705,7 @@ var BruteForceDetector = class {
|
|
|
5704
5705
|
}
|
|
5705
5706
|
settings;
|
|
5706
5707
|
async recordFailedAttempt(ip, email) {
|
|
5707
|
-
if (!this.settings.enabled) {
|
|
5708
|
+
if (!this.settings.enabled || !this.kv) {
|
|
5708
5709
|
return { ipCount: 0, emailCount: 0, shouldLockIP: false, shouldLockEmail: false, isSuspicious: false };
|
|
5709
5710
|
}
|
|
5710
5711
|
const windowMs = this.settings.windowMinutes * 60 * 1e3;
|
|
@@ -5721,7 +5722,7 @@ var BruteForceDetector = class {
|
|
|
5721
5722
|
return { ipCount, emailCount, shouldLockIP, shouldLockEmail, isSuspicious };
|
|
5722
5723
|
}
|
|
5723
5724
|
async isLocked(ip, email) {
|
|
5724
|
-
if (!this.settings.enabled) {
|
|
5725
|
+
if (!this.settings.enabled || !this.kv) {
|
|
5725
5726
|
return { locked: false };
|
|
5726
5727
|
}
|
|
5727
5728
|
const ipLocked = await this.kv.get(`${LOCK_PREFIX}ip:${ip}`);
|
|
@@ -5735,6 +5736,7 @@ var BruteForceDetector = class {
|
|
|
5735
5736
|
return { locked: false };
|
|
5736
5737
|
}
|
|
5737
5738
|
async lockIP(ip) {
|
|
5739
|
+
if (!this.kv) return;
|
|
5738
5740
|
const ttl = this.settings.lockoutDurationMinutes * 60;
|
|
5739
5741
|
await this.kv.put(`${LOCK_PREFIX}ip:${ip}`, JSON.stringify({
|
|
5740
5742
|
lockedAt: Date.now(),
|
|
@@ -5742,6 +5744,7 @@ var BruteForceDetector = class {
|
|
|
5742
5744
|
}), { expirationTtl: ttl });
|
|
5743
5745
|
}
|
|
5744
5746
|
async lockEmail(email) {
|
|
5747
|
+
if (!this.kv) return;
|
|
5745
5748
|
const ttl = this.settings.lockoutDurationMinutes * 60;
|
|
5746
5749
|
await this.kv.put(`${LOCK_PREFIX}email:${email}`, JSON.stringify({
|
|
5747
5750
|
lockedAt: Date.now(),
|
|
@@ -5749,12 +5752,15 @@ var BruteForceDetector = class {
|
|
|
5749
5752
|
}), { expirationTtl: ttl });
|
|
5750
5753
|
}
|
|
5751
5754
|
async unlockIP(ip) {
|
|
5755
|
+
if (!this.kv) return;
|
|
5752
5756
|
await this.kv.delete(`${LOCK_PREFIX}ip:${ip}`);
|
|
5753
5757
|
}
|
|
5754
5758
|
async unlockEmail(email) {
|
|
5759
|
+
if (!this.kv) return;
|
|
5755
5760
|
await this.kv.delete(`${LOCK_PREFIX}email:${email}`);
|
|
5756
5761
|
}
|
|
5757
5762
|
async getActiveLockouts() {
|
|
5763
|
+
if (!this.kv) return [];
|
|
5758
5764
|
const ipLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}ip:` });
|
|
5759
5765
|
const emailLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}email:` });
|
|
5760
5766
|
const lockouts = [];
|
|
@@ -5785,6 +5791,7 @@ var BruteForceDetector = class {
|
|
|
5785
5791
|
return lockouts;
|
|
5786
5792
|
}
|
|
5787
5793
|
async releaseLockout(key) {
|
|
5794
|
+
if (!this.kv) return;
|
|
5788
5795
|
await this.kv.delete(key);
|
|
5789
5796
|
}
|
|
5790
5797
|
isAboveAlertThreshold(count) {
|
|
@@ -6328,6 +6335,33 @@ var SubscriptionService = class {
|
|
|
6328
6335
|
).first();
|
|
6329
6336
|
return this.mapRow(result);
|
|
6330
6337
|
}
|
|
6338
|
+
/**
|
|
6339
|
+
* Upsert a subscription by stripe_subscription_id (INSERT or UPDATE on conflict)
|
|
6340
|
+
*/
|
|
6341
|
+
async upsert(data) {
|
|
6342
|
+
const result = await this.db.prepare(`
|
|
6343
|
+
INSERT INTO subscriptions (user_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, status, current_period_start, current_period_end, cancel_at_period_end)
|
|
6344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6345
|
+
ON CONFLICT(stripe_subscription_id) DO UPDATE SET
|
|
6346
|
+
status = excluded.status,
|
|
6347
|
+
stripe_price_id = excluded.stripe_price_id,
|
|
6348
|
+
current_period_start = excluded.current_period_start,
|
|
6349
|
+
current_period_end = excluded.current_period_end,
|
|
6350
|
+
cancel_at_period_end = excluded.cancel_at_period_end,
|
|
6351
|
+
updated_at = unixepoch()
|
|
6352
|
+
RETURNING *
|
|
6353
|
+
`).bind(
|
|
6354
|
+
data.userId,
|
|
6355
|
+
data.stripeCustomerId,
|
|
6356
|
+
data.stripeSubscriptionId,
|
|
6357
|
+
data.stripePriceId,
|
|
6358
|
+
data.status,
|
|
6359
|
+
data.currentPeriodStart,
|
|
6360
|
+
data.currentPeriodEnd,
|
|
6361
|
+
data.cancelAtPeriodEnd ? 1 : 0
|
|
6362
|
+
).first();
|
|
6363
|
+
return this.mapRow(result);
|
|
6364
|
+
}
|
|
6331
6365
|
/**
|
|
6332
6366
|
* Update a subscription by its Stripe subscription ID
|
|
6333
6367
|
*/
|
|
@@ -6482,24 +6516,183 @@ var SubscriptionService = class {
|
|
|
6482
6516
|
}
|
|
6483
6517
|
};
|
|
6484
6518
|
|
|
6519
|
+
// src/plugins/core-plugins/stripe-plugin/services/stripe-event-service.ts
|
|
6520
|
+
var StripeEventService = class {
|
|
6521
|
+
constructor(db) {
|
|
6522
|
+
this.db = db;
|
|
6523
|
+
}
|
|
6524
|
+
async ensureTable() {
|
|
6525
|
+
await this.db.prepare(`
|
|
6526
|
+
CREATE TABLE IF NOT EXISTS stripe_events (
|
|
6527
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
6528
|
+
stripe_event_id TEXT NOT NULL UNIQUE,
|
|
6529
|
+
type TEXT NOT NULL,
|
|
6530
|
+
object_id TEXT NOT NULL DEFAULT '',
|
|
6531
|
+
object_type TEXT NOT NULL DEFAULT '',
|
|
6532
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
6533
|
+
processed_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
6534
|
+
status TEXT NOT NULL DEFAULT 'processed',
|
|
6535
|
+
error TEXT
|
|
6536
|
+
)
|
|
6537
|
+
`).run();
|
|
6538
|
+
await this.db.prepare(`
|
|
6539
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_type ON stripe_events(type)
|
|
6540
|
+
`).run();
|
|
6541
|
+
await this.db.prepare(`
|
|
6542
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_status ON stripe_events(status)
|
|
6543
|
+
`).run();
|
|
6544
|
+
await this.db.prepare(`
|
|
6545
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_processed_at ON stripe_events(processed_at DESC)
|
|
6546
|
+
`).run();
|
|
6547
|
+
}
|
|
6548
|
+
async log(event) {
|
|
6549
|
+
await this.db.prepare(`
|
|
6550
|
+
INSERT INTO stripe_events (stripe_event_id, type, object_id, object_type, data, status, error)
|
|
6551
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
6552
|
+
ON CONFLICT(stripe_event_id) DO UPDATE SET
|
|
6553
|
+
status = excluded.status,
|
|
6554
|
+
error = excluded.error,
|
|
6555
|
+
processed_at = unixepoch()
|
|
6556
|
+
`).bind(
|
|
6557
|
+
event.stripeEventId,
|
|
6558
|
+
event.type,
|
|
6559
|
+
event.objectId,
|
|
6560
|
+
event.objectType,
|
|
6561
|
+
JSON.stringify(event.data),
|
|
6562
|
+
event.status,
|
|
6563
|
+
event.error || null
|
|
6564
|
+
).run();
|
|
6565
|
+
}
|
|
6566
|
+
async list(filters = {}) {
|
|
6567
|
+
const where = [];
|
|
6568
|
+
const values = [];
|
|
6569
|
+
if (filters.type) {
|
|
6570
|
+
where.push("type = ?");
|
|
6571
|
+
values.push(filters.type);
|
|
6572
|
+
}
|
|
6573
|
+
if (filters.status) {
|
|
6574
|
+
where.push("status = ?");
|
|
6575
|
+
values.push(filters.status);
|
|
6576
|
+
}
|
|
6577
|
+
if (filters.objectId) {
|
|
6578
|
+
where.push("object_id = ?");
|
|
6579
|
+
values.push(filters.objectId);
|
|
6580
|
+
}
|
|
6581
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
6582
|
+
const limit = Math.min(filters.limit || 50, 100);
|
|
6583
|
+
const page = filters.page || 1;
|
|
6584
|
+
const offset = (page - 1) * limit;
|
|
6585
|
+
const countResult = await this.db.prepare(
|
|
6586
|
+
`SELECT COUNT(*) as count FROM stripe_events ${whereClause}`
|
|
6587
|
+
).bind(...values).first();
|
|
6588
|
+
const results = await this.db.prepare(
|
|
6589
|
+
`SELECT * FROM stripe_events ${whereClause} ORDER BY processed_at DESC LIMIT ? OFFSET ?`
|
|
6590
|
+
).bind(...values, limit, offset).all();
|
|
6591
|
+
return {
|
|
6592
|
+
events: (results.results || []).map((r) => this.mapRow(r)),
|
|
6593
|
+
total: countResult?.count || 0
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
async getStats() {
|
|
6597
|
+
const result = await this.db.prepare(`
|
|
6598
|
+
SELECT
|
|
6599
|
+
COUNT(*) as total,
|
|
6600
|
+
SUM(CASE WHEN status = 'processed' THEN 1 ELSE 0 END) as processed,
|
|
6601
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
|
|
6602
|
+
SUM(CASE WHEN status = 'ignored' THEN 1 ELSE 0 END) as ignored
|
|
6603
|
+
FROM stripe_events
|
|
6604
|
+
`).first();
|
|
6605
|
+
return {
|
|
6606
|
+
total: result?.total || 0,
|
|
6607
|
+
processed: result?.processed || 0,
|
|
6608
|
+
failed: result?.failed || 0,
|
|
6609
|
+
ignored: result?.ignored || 0
|
|
6610
|
+
};
|
|
6611
|
+
}
|
|
6612
|
+
async getDistinctTypes() {
|
|
6613
|
+
const results = await this.db.prepare(
|
|
6614
|
+
"SELECT DISTINCT type FROM stripe_events ORDER BY type"
|
|
6615
|
+
).all();
|
|
6616
|
+
return (results.results || []).map((r) => r.type);
|
|
6617
|
+
}
|
|
6618
|
+
mapRow(row) {
|
|
6619
|
+
return {
|
|
6620
|
+
id: row.id,
|
|
6621
|
+
stripeEventId: row.stripe_event_id,
|
|
6622
|
+
type: row.type,
|
|
6623
|
+
objectId: row.object_id,
|
|
6624
|
+
objectType: row.object_type,
|
|
6625
|
+
data: row.data,
|
|
6626
|
+
processedAt: row.processed_at,
|
|
6627
|
+
status: row.status,
|
|
6628
|
+
error: row.error || void 0
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
};
|
|
6632
|
+
|
|
6485
6633
|
// src/plugins/core-plugins/stripe-plugin/components/subscriptions-page.ts
|
|
6486
|
-
|
|
6634
|
+
init_admin_layout_catalyst_template();
|
|
6635
|
+
|
|
6636
|
+
// src/plugins/core-plugins/stripe-plugin/components/tab-bar.ts
|
|
6637
|
+
var TABS = [
|
|
6638
|
+
{ label: "Subscriptions", path: "/admin/plugins/stripe" },
|
|
6639
|
+
{ label: "Events", path: "/admin/plugins/stripe/events" },
|
|
6640
|
+
{ label: "Settings", path: "/admin/plugins/stripe/settings" }
|
|
6641
|
+
];
|
|
6642
|
+
function renderStripeTabBar(currentPath) {
|
|
6643
|
+
const tabs = TABS.map((tab) => {
|
|
6644
|
+
const isActive = currentPath === tab.path || tab.path === "/admin/plugins/stripe" && currentPath === "/admin/plugins/stripe/";
|
|
6645
|
+
return `
|
|
6646
|
+
<a href="${tab.path}"
|
|
6647
|
+
class="${isActive ? "border-cyan-500 text-zinc-950 dark:text-white" : "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600"} whitespace-nowrap border-b-2 px-4 py-3 text-sm font-medium transition-colors">
|
|
6648
|
+
${tab.label}
|
|
6649
|
+
</a>`;
|
|
6650
|
+
}).join("");
|
|
6487
6651
|
return `
|
|
6488
|
-
<div class="
|
|
6652
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 mb-6">
|
|
6653
|
+
<nav class="-mb-px flex gap-x-2" aria-label="Stripe tabs">
|
|
6654
|
+
${tabs}
|
|
6655
|
+
</nav>
|
|
6656
|
+
</div>
|
|
6657
|
+
`;
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
// src/plugins/core-plugins/stripe-plugin/components/subscriptions-page.ts
|
|
6661
|
+
function renderSubscriptionsPage(data) {
|
|
6662
|
+
const { subscriptions, stats, filters, user, version, dynamicMenuItems } = data;
|
|
6663
|
+
const content2 = `
|
|
6664
|
+
<div>
|
|
6665
|
+
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
|
6666
|
+
<div class="sm:flex-auto">
|
|
6667
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
6668
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
6669
|
+
Manage subscriptions, view billing status, and monitor payment events.
|
|
6670
|
+
</p>
|
|
6671
|
+
</div>
|
|
6672
|
+
<div class="mt-4 sm:mt-0 sm:ml-16">
|
|
6673
|
+
<button id="sync-btn" onclick="syncSubscriptions()"
|
|
6674
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
6675
|
+
Sync from Stripe
|
|
6676
|
+
</button>
|
|
6677
|
+
</div>
|
|
6678
|
+
</div>
|
|
6679
|
+
|
|
6680
|
+
${renderStripeTabBar("/admin/plugins/stripe")}
|
|
6681
|
+
|
|
6489
6682
|
<!-- Stats Cards -->
|
|
6490
|
-
<div class="grid grid-cols-1
|
|
6491
|
-
${statsCard("Total", stats.total, "text-
|
|
6492
|
-
${statsCard("Active", stats.active, "text-
|
|
6493
|
-
${statsCard("Trialing", stats.trialing, "text-blue-600")}
|
|
6494
|
-
${statsCard("Past Due", stats.pastDue, "text-
|
|
6495
|
-
${statsCard("Canceled", stats.canceled, "text-red-600")}
|
|
6683
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5 mb-6">
|
|
6684
|
+
${statsCard("Total", stats.total, "text-zinc-950 dark:text-white")}
|
|
6685
|
+
${statsCard("Active", stats.active, "text-emerald-600 dark:text-emerald-400")}
|
|
6686
|
+
${statsCard("Trialing", stats.trialing, "text-blue-600 dark:text-blue-400")}
|
|
6687
|
+
${statsCard("Past Due", stats.pastDue, "text-amber-600 dark:text-amber-400")}
|
|
6688
|
+
${statsCard("Canceled", stats.canceled, "text-red-600 dark:text-red-400")}
|
|
6496
6689
|
</div>
|
|
6497
6690
|
|
|
6498
6691
|
<!-- Filters -->
|
|
6499
|
-
<div class="bg-white
|
|
6692
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-4 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm mb-6">
|
|
6500
6693
|
<form method="GET" class="flex items-center gap-4">
|
|
6501
|
-
<label class="text-sm font-medium text-
|
|
6502
|
-
<select name="status" class="border
|
|
6694
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Status:</label>
|
|
6695
|
+
<select name="status" class="rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-1.5 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10" onchange="this.form.submit()">
|
|
6503
6696
|
<option value="">All</option>
|
|
6504
6697
|
${statusOption("active", filters.status)}
|
|
6505
6698
|
${statusOption("trialing", filters.status)}
|
|
@@ -6512,33 +6705,72 @@ function renderSubscriptionsPage(subscriptions, stats, filters) {
|
|
|
6512
6705
|
</div>
|
|
6513
6706
|
|
|
6514
6707
|
<!-- Subscriptions Table -->
|
|
6515
|
-
<div class="bg-white
|
|
6516
|
-
<table class="min-w-full divide-y divide-
|
|
6517
|
-
<thead
|
|
6708
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-hidden">
|
|
6709
|
+
<table class="min-w-full divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6710
|
+
<thead>
|
|
6518
6711
|
<tr>
|
|
6519
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6520
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6521
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6522
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6523
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6524
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-
|
|
6712
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">User</th>
|
|
6713
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
|
6714
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Price ID</th>
|
|
6715
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Current Period</th>
|
|
6716
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Cancel at End</th>
|
|
6717
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Stripe</th>
|
|
6525
6718
|
</tr>
|
|
6526
6719
|
</thead>
|
|
6527
|
-
<tbody class="
|
|
6528
|
-
${subscriptions.length === 0 ? '<tr><td colspan="6" class="px-6 py-8 text-center text-
|
|
6720
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6721
|
+
${subscriptions.length === 0 ? '<tr><td colspan="6" class="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400">No subscriptions found</td></tr>' : subscriptions.map(renderRow).join("")}
|
|
6529
6722
|
</tbody>
|
|
6530
6723
|
</table>
|
|
6531
6724
|
|
|
6532
6725
|
${renderPagination2(filters.page, filters.totalPages, filters.status)}
|
|
6533
6726
|
</div>
|
|
6727
|
+
|
|
6728
|
+
<div id="sync-message" class="hidden mt-4 rounded-lg p-4 text-sm"></div>
|
|
6534
6729
|
</div>
|
|
6730
|
+
|
|
6731
|
+
<script>
|
|
6732
|
+
async function syncSubscriptions() {
|
|
6733
|
+
const btn = document.getElementById('sync-btn')
|
|
6734
|
+
const msg = document.getElementById('sync-message')
|
|
6735
|
+
btn.disabled = true
|
|
6736
|
+
btn.textContent = 'Syncing...'
|
|
6737
|
+
msg.className = 'hidden mt-4 rounded-lg p-4 text-sm'
|
|
6738
|
+
try {
|
|
6739
|
+
const res = await fetch('/api/stripe/sync-subscriptions', { method: 'POST' })
|
|
6740
|
+
const result = await res.json()
|
|
6741
|
+
if (result.success) {
|
|
6742
|
+
msg.className = 'mt-4 rounded-lg p-4 text-sm bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-1 ring-inset ring-emerald-400/20'
|
|
6743
|
+
msg.textContent = 'Synced ' + result.synced + ' of ' + result.total + ' subscriptions from Stripe.' + (result.errors > 0 ? ' (' + result.errors + ' errors)' : '')
|
|
6744
|
+
setTimeout(() => location.reload(), 1500)
|
|
6745
|
+
} else {
|
|
6746
|
+
msg.className = 'mt-4 rounded-lg p-4 text-sm bg-red-400/10 text-red-500 dark:text-red-400 ring-1 ring-inset ring-red-400/20'
|
|
6747
|
+
msg.textContent = result.error || 'Sync failed.'
|
|
6748
|
+
}
|
|
6749
|
+
} catch {
|
|
6750
|
+
msg.className = 'mt-4 rounded-lg p-4 text-sm bg-red-400/10 text-red-500 dark:text-red-400 ring-1 ring-inset ring-red-400/20'
|
|
6751
|
+
msg.textContent = 'Network error. Please try again.'
|
|
6752
|
+
}
|
|
6753
|
+
btn.disabled = false
|
|
6754
|
+
btn.textContent = 'Sync from Stripe'
|
|
6755
|
+
}
|
|
6756
|
+
</script>
|
|
6535
6757
|
`;
|
|
6758
|
+
const layoutData = {
|
|
6759
|
+
title: "Stripe Subscriptions",
|
|
6760
|
+
pageTitle: "Stripe Subscriptions",
|
|
6761
|
+
currentPath: "/admin/plugins/stripe",
|
|
6762
|
+
user,
|
|
6763
|
+
content: content2,
|
|
6764
|
+
version,
|
|
6765
|
+
dynamicMenuItems
|
|
6766
|
+
};
|
|
6767
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
6536
6768
|
}
|
|
6537
6769
|
function statsCard(label, value, colorClass) {
|
|
6538
6770
|
return `
|
|
6539
|
-
<div class="bg-white
|
|
6540
|
-
<
|
|
6541
|
-
<
|
|
6771
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
|
|
6772
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${label}</p>
|
|
6773
|
+
<p class="mt-2 text-3xl font-bold ${colorClass}">${value}</p>
|
|
6542
6774
|
</div>
|
|
6543
6775
|
`;
|
|
6544
6776
|
}
|
|
@@ -6549,18 +6781,18 @@ function statusOption(value, current) {
|
|
|
6549
6781
|
}
|
|
6550
6782
|
function statusBadge(status) {
|
|
6551
6783
|
const colors = {
|
|
6552
|
-
active: "bg-
|
|
6553
|
-
trialing: "bg-blue-
|
|
6554
|
-
past_due: "bg-
|
|
6555
|
-
canceled: "bg-red-
|
|
6556
|
-
unpaid: "bg-orange-
|
|
6557
|
-
paused: "bg-
|
|
6558
|
-
incomplete: "bg-
|
|
6559
|
-
incomplete_expired: "bg-red-
|
|
6784
|
+
active: "bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-emerald-400/20",
|
|
6785
|
+
trialing: "bg-blue-400/10 text-blue-500 dark:text-blue-400 ring-blue-400/20",
|
|
6786
|
+
past_due: "bg-amber-400/10 text-amber-500 dark:text-amber-400 ring-amber-400/20",
|
|
6787
|
+
canceled: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20",
|
|
6788
|
+
unpaid: "bg-orange-400/10 text-orange-500 dark:text-orange-400 ring-orange-400/20",
|
|
6789
|
+
paused: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20",
|
|
6790
|
+
incomplete: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20",
|
|
6791
|
+
incomplete_expired: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20"
|
|
6560
6792
|
};
|
|
6561
|
-
const color = colors[status] || "bg-
|
|
6793
|
+
const color = colors[status] || "bg-zinc-400/10 text-zinc-500 ring-zinc-400/20";
|
|
6562
6794
|
const label = status.replace("_", " ");
|
|
6563
|
-
return `<span class="inline-flex items-center px-2
|
|
6795
|
+
return `<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset ${color}">${label}</span>`;
|
|
6564
6796
|
}
|
|
6565
6797
|
function formatDate(timestamp) {
|
|
6566
6798
|
if (!timestamp) return "-";
|
|
@@ -6572,23 +6804,23 @@ function formatDate(timestamp) {
|
|
|
6572
6804
|
}
|
|
6573
6805
|
function renderRow(sub) {
|
|
6574
6806
|
return `
|
|
6575
|
-
<tr>
|
|
6807
|
+
<tr class="hover:bg-zinc-950/[0.025] dark:hover:bg-white/[0.025]">
|
|
6576
6808
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
6577
|
-
<div class="text-sm font-medium text-
|
|
6578
|
-
<div class="text-xs text-
|
|
6809
|
+
<div class="text-sm font-medium text-zinc-950 dark:text-white">${sub.userEmail || sub.userId}</div>
|
|
6810
|
+
<div class="text-xs text-zinc-500 dark:text-zinc-400">${sub.stripeCustomerId}</div>
|
|
6579
6811
|
</td>
|
|
6580
6812
|
<td class="px-6 py-4 whitespace-nowrap">${statusBadge(sub.status)}</td>
|
|
6581
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-
|
|
6582
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-
|
|
6813
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">${sub.stripePriceId}</td>
|
|
6814
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
|
6583
6815
|
${formatDate(sub.currentPeriodStart)} - ${formatDate(sub.currentPeriodEnd)}
|
|
6584
6816
|
</td>
|
|
6585
6817
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6586
|
-
${sub.cancelAtPeriodEnd ? '<span class="text-
|
|
6818
|
+
${sub.cancelAtPeriodEnd ? '<span class="text-amber-500 dark:text-amber-400 font-medium">Yes</span>' : '<span class="text-zinc-400 dark:text-zinc-500">No</span>'}
|
|
6587
6819
|
</td>
|
|
6588
6820
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6589
6821
|
<a href="https://dashboard.stripe.com/subscriptions/${sub.stripeSubscriptionId}"
|
|
6590
6822
|
target="_blank" rel="noopener noreferrer"
|
|
6591
|
-
class="text-
|
|
6823
|
+
class="text-cyan-600 dark:text-cyan-400 hover:text-cyan-500 dark:hover:text-cyan-300">
|
|
6592
6824
|
View in Stripe
|
|
6593
6825
|
</a>
|
|
6594
6826
|
</td>
|
|
@@ -6599,13 +6831,161 @@ function renderPagination2(page, totalPages, status) {
|
|
|
6599
6831
|
if (totalPages <= 1) return "";
|
|
6600
6832
|
const params = status ? `&status=${status}` : "";
|
|
6601
6833
|
return `
|
|
6602
|
-
<div class="
|
|
6603
|
-
<div class="text-sm text-
|
|
6834
|
+
<div class="px-6 py-3 flex items-center justify-between border-t border-zinc-950/5 dark:border-white/5">
|
|
6835
|
+
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
6604
6836
|
Page ${page} of ${totalPages}
|
|
6605
6837
|
</div>
|
|
6606
6838
|
<div class="flex gap-2">
|
|
6607
|
-
${page > 1 ? `<a href="?page=${page - 1}${params}" class="px-3 py-1
|
|
6608
|
-
${page < totalPages ? `<a href="?page=${page + 1}${params}" class="px-3 py-1
|
|
6839
|
+
${page > 1 ? `<a href="?page=${page - 1}${params}" class="px-3 py-1 rounded-lg text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-800">Previous</a>` : ""}
|
|
6840
|
+
${page < totalPages ? `<a href="?page=${page + 1}${params}" class="px-3 py-1 rounded-lg text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-800">Next</a>` : ""}
|
|
6841
|
+
</div>
|
|
6842
|
+
</div>
|
|
6843
|
+
`;
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
// src/plugins/core-plugins/stripe-plugin/components/events-page.ts
|
|
6847
|
+
init_admin_layout_catalyst_template();
|
|
6848
|
+
function renderEventsPage(data) {
|
|
6849
|
+
const { events, stats, types, filters, user, version, dynamicMenuItems } = data;
|
|
6850
|
+
const content2 = `
|
|
6851
|
+
<div>
|
|
6852
|
+
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
|
6853
|
+
<div class="sm:flex-auto">
|
|
6854
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
6855
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
6856
|
+
Webhook event log showing all processed, failed, and ignored Stripe events.
|
|
6857
|
+
</p>
|
|
6858
|
+
</div>
|
|
6859
|
+
</div>
|
|
6860
|
+
|
|
6861
|
+
${renderStripeTabBar("/admin/plugins/stripe/events")}
|
|
6862
|
+
|
|
6863
|
+
<!-- Stats Cards -->
|
|
6864
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
|
6865
|
+
${eventStatsCard("Total Events", stats.total, "text-zinc-950 dark:text-white")}
|
|
6866
|
+
${eventStatsCard("Processed", stats.processed, "text-emerald-600 dark:text-emerald-400")}
|
|
6867
|
+
${eventStatsCard("Failed", stats.failed, "text-red-600 dark:text-red-400")}
|
|
6868
|
+
${eventStatsCard("Ignored", stats.ignored, "text-zinc-500 dark:text-zinc-400")}
|
|
6869
|
+
</div>
|
|
6870
|
+
|
|
6871
|
+
<!-- Filters -->
|
|
6872
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-4 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm mb-6">
|
|
6873
|
+
<form method="GET" class="flex items-center gap-4 flex-wrap">
|
|
6874
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Type:</label>
|
|
6875
|
+
<select name="type" class="rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-1.5 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10" onchange="this.form.submit()">
|
|
6876
|
+
<option value="">All</option>
|
|
6877
|
+
${types.map((t) => `<option value="${t}" ${t === filters.type ? "selected" : ""}>${t}</option>`).join("")}
|
|
6878
|
+
</select>
|
|
6879
|
+
|
|
6880
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Status:</label>
|
|
6881
|
+
<select name="status" class="rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-1.5 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10" onchange="this.form.submit()">
|
|
6882
|
+
<option value="">All</option>
|
|
6883
|
+
${eventStatusOption("processed", filters.status)}
|
|
6884
|
+
${eventStatusOption("failed", filters.status)}
|
|
6885
|
+
${eventStatusOption("ignored", filters.status)}
|
|
6886
|
+
</select>
|
|
6887
|
+
</form>
|
|
6888
|
+
</div>
|
|
6889
|
+
|
|
6890
|
+
<!-- Events Table -->
|
|
6891
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-hidden">
|
|
6892
|
+
<table class="min-w-full divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6893
|
+
<thead>
|
|
6894
|
+
<tr>
|
|
6895
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Time</th>
|
|
6896
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Type</th>
|
|
6897
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Object</th>
|
|
6898
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
|
6899
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Event ID</th>
|
|
6900
|
+
</tr>
|
|
6901
|
+
</thead>
|
|
6902
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6903
|
+
${events.length === 0 ? '<tr><td colspan="5" class="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400">No events recorded yet</td></tr>' : events.map(renderEventRow).join("")}
|
|
6904
|
+
</tbody>
|
|
6905
|
+
</table>
|
|
6906
|
+
|
|
6907
|
+
${renderEventPagination(filters.page, filters.totalPages, filters.type, filters.status)}
|
|
6908
|
+
</div>
|
|
6909
|
+
</div>
|
|
6910
|
+
`;
|
|
6911
|
+
const layoutData = {
|
|
6912
|
+
title: "Stripe Events",
|
|
6913
|
+
pageTitle: "Stripe Events",
|
|
6914
|
+
currentPath: "/admin/plugins/stripe",
|
|
6915
|
+
user,
|
|
6916
|
+
content: content2,
|
|
6917
|
+
version,
|
|
6918
|
+
dynamicMenuItems
|
|
6919
|
+
};
|
|
6920
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
6921
|
+
}
|
|
6922
|
+
function eventStatsCard(label, value, colorClass) {
|
|
6923
|
+
return `
|
|
6924
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
|
|
6925
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${label}</p>
|
|
6926
|
+
<p class="mt-2 text-3xl font-bold ${colorClass}">${value}</p>
|
|
6927
|
+
</div>
|
|
6928
|
+
`;
|
|
6929
|
+
}
|
|
6930
|
+
function eventStatusOption(value, current) {
|
|
6931
|
+
const selected = value === current ? "selected" : "";
|
|
6932
|
+
const label = value.charAt(0).toUpperCase() + value.slice(1);
|
|
6933
|
+
return `<option value="${value}" ${selected}>${label}</option>`;
|
|
6934
|
+
}
|
|
6935
|
+
function eventStatusBadge(status) {
|
|
6936
|
+
const colors = {
|
|
6937
|
+
processed: "bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-emerald-400/20",
|
|
6938
|
+
failed: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20",
|
|
6939
|
+
ignored: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20"
|
|
6940
|
+
};
|
|
6941
|
+
const color = colors[status] || "bg-zinc-400/10 text-zinc-500 ring-zinc-400/20";
|
|
6942
|
+
return `<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset ${color}">${status}</span>`;
|
|
6943
|
+
}
|
|
6944
|
+
function formatTimestamp3(timestamp) {
|
|
6945
|
+
if (!timestamp) return "-";
|
|
6946
|
+
const d = new Date(timestamp * 1e3);
|
|
6947
|
+
return d.toLocaleString("en-US", {
|
|
6948
|
+
month: "short",
|
|
6949
|
+
day: "numeric",
|
|
6950
|
+
year: "numeric",
|
|
6951
|
+
hour: "2-digit",
|
|
6952
|
+
minute: "2-digit",
|
|
6953
|
+
second: "2-digit"
|
|
6954
|
+
});
|
|
6955
|
+
}
|
|
6956
|
+
function renderEventRow(event) {
|
|
6957
|
+
const errorTooltip = event.error ? ` title="${event.error.replace(/"/g, """)}"` : "";
|
|
6958
|
+
return `
|
|
6959
|
+
<tr class="hover:bg-zinc-950/[0.025] dark:hover:bg-white/[0.025]"${errorTooltip}>
|
|
6960
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
|
6961
|
+
${formatTimestamp3(event.processedAt)}
|
|
6962
|
+
</td>
|
|
6963
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6964
|
+
<span class="text-sm font-mono text-zinc-950 dark:text-white">${event.type}</span>
|
|
6965
|
+
</td>
|
|
6966
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6967
|
+
<div class="text-sm font-mono text-zinc-500 dark:text-zinc-400">${event.objectId || "-"}</div>
|
|
6968
|
+
<div class="text-xs text-zinc-400 dark:text-zinc-500">${event.objectType}</div>
|
|
6969
|
+
</td>
|
|
6970
|
+
<td class="px-6 py-4 whitespace-nowrap">${eventStatusBadge(event.status)}</td>
|
|
6971
|
+
<td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-zinc-400 dark:text-zinc-500">${event.stripeEventId}</td>
|
|
6972
|
+
</tr>
|
|
6973
|
+
`;
|
|
6974
|
+
}
|
|
6975
|
+
function renderEventPagination(page, totalPages, type, status) {
|
|
6976
|
+
if (totalPages <= 1) return "";
|
|
6977
|
+
const params = [];
|
|
6978
|
+
if (type) params.push(`type=${type}`);
|
|
6979
|
+
if (status) params.push(`status=${status}`);
|
|
6980
|
+
const extra = params.length > 0 ? `&${params.join("&")}` : "";
|
|
6981
|
+
return `
|
|
6982
|
+
<div class="px-6 py-3 flex items-center justify-between border-t border-zinc-950/5 dark:border-white/5">
|
|
6983
|
+
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
6984
|
+
Page ${page} of ${totalPages}
|
|
6985
|
+
</div>
|
|
6986
|
+
<div class="flex gap-2">
|
|
6987
|
+
${page > 1 ? `<a href="?page=${page - 1}${extra}" class="px-3 py-1 rounded-lg text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-800">Previous</a>` : ""}
|
|
6988
|
+
${page < totalPages ? `<a href="?page=${page + 1}${extra}" class="px-3 py-1 rounded-lg text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-800">Next</a>` : ""}
|
|
6609
6989
|
</div>
|
|
6610
6990
|
</div>
|
|
6611
6991
|
`;
|
|
@@ -6613,6 +6993,7 @@ function renderPagination2(page, totalPages, status) {
|
|
|
6613
6993
|
|
|
6614
6994
|
// src/plugins/core-plugins/stripe-plugin/types.ts
|
|
6615
6995
|
var DEFAULT_SETTINGS3 = {
|
|
6996
|
+
stripePublishableKey: "",
|
|
6616
6997
|
stripeSecretKey: "",
|
|
6617
6998
|
stripeWebhookSecret: "",
|
|
6618
6999
|
stripePriceId: "",
|
|
@@ -6630,8 +7011,21 @@ adminRoutes3.use("*", async (c, next) => {
|
|
|
6630
7011
|
}
|
|
6631
7012
|
return next();
|
|
6632
7013
|
});
|
|
7014
|
+
async function getSettings3(db) {
|
|
7015
|
+
try {
|
|
7016
|
+
const pluginService = new PluginService(db);
|
|
7017
|
+
const plugin2 = await pluginService.getPlugin("stripe");
|
|
7018
|
+
if (plugin2?.settings) {
|
|
7019
|
+
const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
|
|
7020
|
+
return { ...DEFAULT_SETTINGS3, ...settings };
|
|
7021
|
+
}
|
|
7022
|
+
} catch {
|
|
7023
|
+
}
|
|
7024
|
+
return DEFAULT_SETTINGS3;
|
|
7025
|
+
}
|
|
6633
7026
|
adminRoutes3.get("/", async (c) => {
|
|
6634
7027
|
const db = c.env.DB;
|
|
7028
|
+
const user = c.get("user");
|
|
6635
7029
|
const subscriptionService = new SubscriptionService(db);
|
|
6636
7030
|
await subscriptionService.ensureTable();
|
|
6637
7031
|
const page = parseInt(c.req.query("page") || "1");
|
|
@@ -6642,13 +7036,152 @@ adminRoutes3.get("/", async (c) => {
|
|
|
6642
7036
|
subscriptionService.getStats()
|
|
6643
7037
|
]);
|
|
6644
7038
|
const totalPages = Math.ceil(total / limit);
|
|
6645
|
-
const html = renderSubscriptionsPage(
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
totalPages
|
|
7039
|
+
const html = renderSubscriptionsPage({
|
|
7040
|
+
subscriptions,
|
|
7041
|
+
stats,
|
|
7042
|
+
filters: { status: statusFilter, page, totalPages },
|
|
7043
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7044
|
+
version: c.get("appVersion"),
|
|
7045
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7046
|
+
});
|
|
7047
|
+
return c.html(html);
|
|
7048
|
+
});
|
|
7049
|
+
adminRoutes3.get("/events", async (c) => {
|
|
7050
|
+
const db = c.env.DB;
|
|
7051
|
+
const user = c.get("user");
|
|
7052
|
+
const eventService = new StripeEventService(db);
|
|
7053
|
+
await eventService.ensureTable();
|
|
7054
|
+
const page = parseInt(c.req.query("page") || "1");
|
|
7055
|
+
const limit = 50;
|
|
7056
|
+
const typeFilter = c.req.query("type") || void 0;
|
|
7057
|
+
const statusFilter = c.req.query("status");
|
|
7058
|
+
const [{ events, total }, stats, types] = await Promise.all([
|
|
7059
|
+
eventService.list({ type: typeFilter, status: statusFilter, page, limit }),
|
|
7060
|
+
eventService.getStats(),
|
|
7061
|
+
eventService.getDistinctTypes()
|
|
7062
|
+
]);
|
|
7063
|
+
const totalPages = Math.ceil(total / limit);
|
|
7064
|
+
const html = renderEventsPage({
|
|
7065
|
+
events,
|
|
7066
|
+
stats,
|
|
7067
|
+
types,
|
|
7068
|
+
filters: { type: typeFilter, status: statusFilter, page, totalPages },
|
|
7069
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7070
|
+
version: c.get("appVersion"),
|
|
7071
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
6649
7072
|
});
|
|
6650
7073
|
return c.html(html);
|
|
6651
7074
|
});
|
|
7075
|
+
adminRoutes3.get("/settings", async (c) => {
|
|
7076
|
+
const db = c.env.DB;
|
|
7077
|
+
const user = c.get("user");
|
|
7078
|
+
const settings = await getSettings3(db);
|
|
7079
|
+
const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-UMTIN66R.js');
|
|
7080
|
+
const content2 = `
|
|
7081
|
+
<div>
|
|
7082
|
+
<div class="mb-6">
|
|
7083
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
7084
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
7085
|
+
Configure your Stripe API keys and checkout options.
|
|
7086
|
+
</p>
|
|
7087
|
+
</div>
|
|
7088
|
+
|
|
7089
|
+
${renderStripeTabBar("/admin/plugins/stripe/settings")}
|
|
7090
|
+
|
|
7091
|
+
<div id="settings-message" class="hidden mb-4 rounded-lg p-4 text-sm"></div>
|
|
7092
|
+
|
|
7093
|
+
<form id="stripe-settings-form" class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
7094
|
+
<div class="p-6 space-y-5">
|
|
7095
|
+
<div>
|
|
7096
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Publishable Key</label>
|
|
7097
|
+
<input type="text" name="stripePublishableKey" value="${settings.stripePublishableKey}"
|
|
7098
|
+
placeholder="pk_..."
|
|
7099
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7100
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Your Stripe publishable key (starts with pk_)</p>
|
|
7101
|
+
</div>
|
|
7102
|
+
|
|
7103
|
+
<div>
|
|
7104
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Secret Key</label>
|
|
7105
|
+
<input type="password" name="stripeSecretKey" value="${settings.stripeSecretKey}"
|
|
7106
|
+
placeholder="sk_..."
|
|
7107
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7108
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Your Stripe secret API key (starts with sk_)</p>
|
|
7109
|
+
</div>
|
|
7110
|
+
|
|
7111
|
+
<div>
|
|
7112
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Webhook Signing Secret</label>
|
|
7113
|
+
<input type="password" name="stripeWebhookSecret" value="${settings.stripeWebhookSecret}"
|
|
7114
|
+
placeholder="whsec_..."
|
|
7115
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7116
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Stripe webhook endpoint signing secret (starts with whsec_)</p>
|
|
7117
|
+
</div>
|
|
7118
|
+
|
|
7119
|
+
<div>
|
|
7120
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Default Price ID</label>
|
|
7121
|
+
<input type="text" name="stripePriceId" value="${settings.stripePriceId || ""}"
|
|
7122
|
+
placeholder="price_..."
|
|
7123
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7124
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Default Stripe Price ID for checkout sessions (optional)</p>
|
|
7125
|
+
</div>
|
|
7126
|
+
|
|
7127
|
+
<div>
|
|
7128
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Checkout Success URL</label>
|
|
7129
|
+
<input type="text" name="successUrl" value="${settings.successUrl}"
|
|
7130
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7131
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Redirect URL after successful checkout</p>
|
|
7132
|
+
</div>
|
|
7133
|
+
|
|
7134
|
+
<div>
|
|
7135
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Checkout Cancel URL</label>
|
|
7136
|
+
<input type="text" name="cancelUrl" value="${settings.cancelUrl}"
|
|
7137
|
+
class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3.5 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:ring-2 focus:ring-cyan-500" />
|
|
7138
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Redirect URL if checkout is cancelled</p>
|
|
7139
|
+
</div>
|
|
7140
|
+
</div>
|
|
7141
|
+
|
|
7142
|
+
<div class="px-6 py-4 flex justify-end">
|
|
7143
|
+
<button type="submit"
|
|
7144
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
7145
|
+
Save Settings
|
|
7146
|
+
</button>
|
|
7147
|
+
</div>
|
|
7148
|
+
</form>
|
|
7149
|
+
</div>
|
|
7150
|
+
|
|
7151
|
+
<script>
|
|
7152
|
+
document.getElementById('stripe-settings-form').addEventListener('submit', async (e) => {
|
|
7153
|
+
e.preventDefault()
|
|
7154
|
+
const form = e.target
|
|
7155
|
+
const data = Object.fromEntries(new FormData(form))
|
|
7156
|
+
const msg = document.getElementById('settings-message')
|
|
7157
|
+
try {
|
|
7158
|
+
const res = await fetch('/admin/plugins/stripe/settings', {
|
|
7159
|
+
method: 'POST',
|
|
7160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7161
|
+
body: JSON.stringify(data)
|
|
7162
|
+
})
|
|
7163
|
+
const result = await res.json()
|
|
7164
|
+
msg.className = result.success
|
|
7165
|
+
? 'mb-4 rounded-lg p-4 text-sm bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-1 ring-inset ring-emerald-400/20'
|
|
7166
|
+
: 'mb-4 rounded-lg p-4 text-sm bg-red-400/10 text-red-500 dark:text-red-400 ring-1 ring-inset ring-red-400/20'
|
|
7167
|
+
msg.textContent = result.success ? 'Settings saved successfully.' : (result.error || 'Failed to save settings.')
|
|
7168
|
+
} catch {
|
|
7169
|
+
msg.className = 'mb-4 rounded-lg p-4 text-sm bg-red-400/10 text-red-500 dark:text-red-400 ring-1 ring-inset ring-red-400/20'
|
|
7170
|
+
msg.textContent = 'Network error. Please try again.'
|
|
7171
|
+
}
|
|
7172
|
+
})
|
|
7173
|
+
</script>
|
|
7174
|
+
`;
|
|
7175
|
+
return c.html(renderAdminLayoutCatalyst2({
|
|
7176
|
+
title: "Stripe Settings",
|
|
7177
|
+
pageTitle: "Stripe Settings",
|
|
7178
|
+
currentPath: "/admin/plugins/stripe",
|
|
7179
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7180
|
+
content: content2,
|
|
7181
|
+
version: c.get("appVersion"),
|
|
7182
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7183
|
+
}));
|
|
7184
|
+
});
|
|
6652
7185
|
adminRoutes3.post("/settings", async (c) => {
|
|
6653
7186
|
try {
|
|
6654
7187
|
const body = await c.req.json();
|
|
@@ -6738,6 +7271,30 @@ var StripeAPI = class {
|
|
|
6738
7271
|
}
|
|
6739
7272
|
return this.request("POST", "/customers", body);
|
|
6740
7273
|
}
|
|
7274
|
+
/**
|
|
7275
|
+
* List subscriptions with pagination (auto-expands across pages)
|
|
7276
|
+
*/
|
|
7277
|
+
async listSubscriptions(params) {
|
|
7278
|
+
const qs = new URLSearchParams();
|
|
7279
|
+
qs.append("limit", String(params?.limit || 100));
|
|
7280
|
+
if (params?.status) qs.append("status", params.status);
|
|
7281
|
+
if (params?.startingAfter) qs.append("starting_after", params.startingAfter);
|
|
7282
|
+
return this.request("GET", `/subscriptions?${qs.toString()}`);
|
|
7283
|
+
}
|
|
7284
|
+
/**
|
|
7285
|
+
* Fetch ALL subscriptions from Stripe (handles pagination automatically)
|
|
7286
|
+
*/
|
|
7287
|
+
async listAllSubscriptions() {
|
|
7288
|
+
const all = [];
|
|
7289
|
+
let startingAfter;
|
|
7290
|
+
while (true) {
|
|
7291
|
+
const result = await this.listSubscriptions({ limit: 100, startingAfter });
|
|
7292
|
+
all.push(...result.data);
|
|
7293
|
+
if (!result.has_more || result.data.length === 0) break;
|
|
7294
|
+
startingAfter = result.data[result.data.length - 1].id;
|
|
7295
|
+
}
|
|
7296
|
+
return all;
|
|
7297
|
+
}
|
|
6741
7298
|
/**
|
|
6742
7299
|
* Search for a customer by email
|
|
6743
7300
|
*/
|
|
@@ -6776,7 +7333,7 @@ function timingSafeEqual(a, b) {
|
|
|
6776
7333
|
|
|
6777
7334
|
// src/plugins/core-plugins/stripe-plugin/routes/api.ts
|
|
6778
7335
|
var apiRoutes3 = new Hono();
|
|
6779
|
-
async function
|
|
7336
|
+
async function getSettings4(db) {
|
|
6780
7337
|
try {
|
|
6781
7338
|
const pluginService = new PluginService(db);
|
|
6782
7339
|
const plugin2 = await pluginService.getPlugin("stripe");
|
|
@@ -6803,7 +7360,7 @@ function mapStripeStatus(status) {
|
|
|
6803
7360
|
}
|
|
6804
7361
|
apiRoutes3.post("/webhook", async (c) => {
|
|
6805
7362
|
const db = c.env.DB;
|
|
6806
|
-
const settings = await
|
|
7363
|
+
const settings = await getSettings4(db);
|
|
6807
7364
|
if (!settings.stripeWebhookSecret) {
|
|
6808
7365
|
return c.json({ error: "Webhook secret not configured" }, 500);
|
|
6809
7366
|
}
|
|
@@ -6816,7 +7373,11 @@ apiRoutes3.post("/webhook", async (c) => {
|
|
|
6816
7373
|
}
|
|
6817
7374
|
const event = JSON.parse(rawBody);
|
|
6818
7375
|
const subscriptionService = new SubscriptionService(db);
|
|
6819
|
-
|
|
7376
|
+
const eventService = new StripeEventService(db);
|
|
7377
|
+
await Promise.all([subscriptionService.ensureTable(), eventService.ensureTable()]);
|
|
7378
|
+
const obj = event.data.object;
|
|
7379
|
+
const objectId = obj?.id || "";
|
|
7380
|
+
const objectType = obj?.object || event.type.split(".")[0] || "";
|
|
6820
7381
|
try {
|
|
6821
7382
|
switch (event.type) {
|
|
6822
7383
|
case "customer.subscription.created": {
|
|
@@ -6891,8 +7452,35 @@ apiRoutes3.post("/webhook", async (c) => {
|
|
|
6891
7452
|
}
|
|
6892
7453
|
default:
|
|
6893
7454
|
console.log(`[Stripe] Unhandled event type: ${event.type}`);
|
|
6894
|
-
|
|
7455
|
+
await eventService.log({
|
|
7456
|
+
stripeEventId: event.id,
|
|
7457
|
+
type: event.type,
|
|
7458
|
+
objectId,
|
|
7459
|
+
objectType,
|
|
7460
|
+
data: event.data.object,
|
|
7461
|
+
status: "ignored"
|
|
7462
|
+
});
|
|
7463
|
+
return c.json({ received: true });
|
|
7464
|
+
}
|
|
7465
|
+
await eventService.log({
|
|
7466
|
+
stripeEventId: event.id,
|
|
7467
|
+
type: event.type,
|
|
7468
|
+
objectId,
|
|
7469
|
+
objectType,
|
|
7470
|
+
data: event.data.object,
|
|
7471
|
+
status: "processed"
|
|
7472
|
+
});
|
|
6895
7473
|
} catch (error) {
|
|
7474
|
+
await eventService.log({
|
|
7475
|
+
stripeEventId: event.id,
|
|
7476
|
+
type: event.type,
|
|
7477
|
+
objectId,
|
|
7478
|
+
objectType,
|
|
7479
|
+
data: event.data.object,
|
|
7480
|
+
status: "failed",
|
|
7481
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7482
|
+
}).catch(() => {
|
|
7483
|
+
});
|
|
6896
7484
|
console.error(`[Stripe] Error processing webhook event ${event.type}:`, error);
|
|
6897
7485
|
return c.json({ error: "Webhook processing failed" }, 500);
|
|
6898
7486
|
}
|
|
@@ -6902,7 +7490,7 @@ apiRoutes3.post("/create-checkout-session", requireAuth(), async (c) => {
|
|
|
6902
7490
|
const db = c.env.DB;
|
|
6903
7491
|
const user = c.get("user");
|
|
6904
7492
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
6905
|
-
const settings = await
|
|
7493
|
+
const settings = await getSettings4(db);
|
|
6906
7494
|
if (!settings.stripeSecretKey) {
|
|
6907
7495
|
return c.json({ error: "Stripe not configured" }, 500);
|
|
6908
7496
|
}
|
|
@@ -6948,32 +7536,100 @@ apiRoutes3.get("/subscription", requireAuth(), async (c) => {
|
|
|
6948
7536
|
if (!subscription) {
|
|
6949
7537
|
return c.json({ subscription: null });
|
|
6950
7538
|
}
|
|
6951
|
-
return c.json({ subscription });
|
|
7539
|
+
return c.json({ subscription });
|
|
7540
|
+
});
|
|
7541
|
+
apiRoutes3.get("/subscriptions", requireAuth(), async (c) => {
|
|
7542
|
+
const user = c.get("user");
|
|
7543
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7544
|
+
const db = c.env.DB;
|
|
7545
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7546
|
+
await subscriptionService.ensureTable();
|
|
7547
|
+
const filters = {
|
|
7548
|
+
status: c.req.query("status"),
|
|
7549
|
+
page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
|
|
7550
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
|
|
7551
|
+
sortBy: c.req.query("sortBy") || "created_at",
|
|
7552
|
+
sortOrder: c.req.query("sortOrder") || "desc"
|
|
7553
|
+
};
|
|
7554
|
+
const result = await subscriptionService.list(filters);
|
|
7555
|
+
return c.json(result);
|
|
7556
|
+
});
|
|
7557
|
+
apiRoutes3.get("/stats", requireAuth(), async (c) => {
|
|
7558
|
+
const user = c.get("user");
|
|
7559
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7560
|
+
const db = c.env.DB;
|
|
7561
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7562
|
+
await subscriptionService.ensureTable();
|
|
7563
|
+
const stats = await subscriptionService.getStats();
|
|
7564
|
+
return c.json(stats);
|
|
7565
|
+
});
|
|
7566
|
+
apiRoutes3.post("/sync-subscriptions", requireAuth(), async (c) => {
|
|
7567
|
+
const user = c.get("user");
|
|
7568
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7569
|
+
const db = c.env.DB;
|
|
7570
|
+
const settings = await getSettings4(db);
|
|
7571
|
+
if (!settings.stripeSecretKey) {
|
|
7572
|
+
return c.json({ error: "Stripe secret key not configured" }, 400);
|
|
7573
|
+
}
|
|
7574
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
7575
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7576
|
+
await subscriptionService.ensureTable();
|
|
7577
|
+
try {
|
|
7578
|
+
const allSubs = await stripeApi.listAllSubscriptions();
|
|
7579
|
+
let synced = 0;
|
|
7580
|
+
let errors = 0;
|
|
7581
|
+
for (const sub of allSubs) {
|
|
7582
|
+
try {
|
|
7583
|
+
const userId = sub.metadata?.sonicjs_user_id || await subscriptionService.getUserIdByStripeCustomer(sub.customer) || "";
|
|
7584
|
+
await subscriptionService.upsert({
|
|
7585
|
+
userId,
|
|
7586
|
+
stripeCustomerId: typeof sub.customer === "string" ? sub.customer : sub.customer.id,
|
|
7587
|
+
stripeSubscriptionId: sub.id,
|
|
7588
|
+
stripePriceId: sub.items?.data?.[0]?.price?.id || "",
|
|
7589
|
+
status: mapStripeStatus(sub.status),
|
|
7590
|
+
currentPeriodStart: sub.current_period_start,
|
|
7591
|
+
currentPeriodEnd: sub.current_period_end,
|
|
7592
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
7593
|
+
});
|
|
7594
|
+
synced++;
|
|
7595
|
+
} catch (err) {
|
|
7596
|
+
console.error(`[Stripe Sync] Failed to upsert subscription ${sub.id}:`, err);
|
|
7597
|
+
errors++;
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
return c.json({
|
|
7601
|
+
success: true,
|
|
7602
|
+
total: allSubs.length,
|
|
7603
|
+
synced,
|
|
7604
|
+
errors
|
|
7605
|
+
});
|
|
7606
|
+
} catch (error) {
|
|
7607
|
+
console.error("[Stripe Sync] Error:", error);
|
|
7608
|
+
return c.json({
|
|
7609
|
+
success: false,
|
|
7610
|
+
error: error instanceof Error ? error.message : "Sync failed"
|
|
7611
|
+
}, 500);
|
|
7612
|
+
}
|
|
6952
7613
|
});
|
|
6953
|
-
apiRoutes3.get("/
|
|
7614
|
+
apiRoutes3.get("/events", requireAuth(), async (c) => {
|
|
6954
7615
|
const user = c.get("user");
|
|
6955
7616
|
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
6956
7617
|
const db = c.env.DB;
|
|
6957
|
-
const
|
|
6958
|
-
await
|
|
7618
|
+
const eventService = new StripeEventService(db);
|
|
7619
|
+
await eventService.ensureTable();
|
|
6959
7620
|
const filters = {
|
|
6960
|
-
|
|
7621
|
+
type: c.req.query("type") || void 0,
|
|
7622
|
+
status: c.req.query("status") || void 0,
|
|
7623
|
+
objectId: c.req.query("objectId") || void 0,
|
|
6961
7624
|
page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
|
|
6962
|
-
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50
|
|
6963
|
-
sortBy: c.req.query("sortBy") || "created_at",
|
|
6964
|
-
sortOrder: c.req.query("sortOrder") || "desc"
|
|
7625
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50
|
|
6965
7626
|
};
|
|
6966
|
-
const result = await
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
const db = c.env.DB;
|
|
6973
|
-
const subscriptionService = new SubscriptionService(db);
|
|
6974
|
-
await subscriptionService.ensureTable();
|
|
6975
|
-
const stats = await subscriptionService.getStats();
|
|
6976
|
-
return c.json(stats);
|
|
7627
|
+
const [result, stats, types] = await Promise.all([
|
|
7628
|
+
eventService.list(filters),
|
|
7629
|
+
eventService.getStats(),
|
|
7630
|
+
eventService.getDistinctTypes()
|
|
7631
|
+
]);
|
|
7632
|
+
return c.json({ ...result, stats, types });
|
|
6977
7633
|
});
|
|
6978
7634
|
|
|
6979
7635
|
// src/plugins/core-plugins/stripe-plugin/index.ts
|
|
@@ -7021,13 +7677,35 @@ function createStripePlugin() {
|
|
|
7021
7677
|
var stripePlugin = createStripePlugin();
|
|
7022
7678
|
|
|
7023
7679
|
// src/middleware/plugin-menu.ts
|
|
7024
|
-
var
|
|
7025
|
-
|
|
7026
|
-
|
|
7680
|
+
var REGISTRY_MENU_PLUGINS = Object.values(PLUGIN_REGISTRY).filter((p) => p.adminMenu !== null).map((p) => ({
|
|
7681
|
+
codeName: p.codeName,
|
|
7682
|
+
label: p.adminMenu.label,
|
|
7683
|
+
path: p.adminMenu.path,
|
|
7684
|
+
icon: p.adminMenu.icon,
|
|
7685
|
+
order: p.adminMenu.order
|
|
7686
|
+
}));
|
|
7687
|
+
var ICON_SVG = {
|
|
7688
|
+
"magnifying-glass": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>',
|
|
7689
|
+
"chart-bar": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg>',
|
|
7690
|
+
"image": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/></svg>',
|
|
7691
|
+
"palette": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z"/></svg>',
|
|
7692
|
+
"envelope": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>',
|
|
7693
|
+
"hand-raised": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.05 4.575a1.575 1.575 0 1 0-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 0 1 3.15 0v1.5m-3.15 0 .075 5.925m3.075-5.925v2.925m0-2.925a1.575 1.575 0 0 1 3.15 0V9.9m-3.15-2.4v5.325M16.5 9.9a1.575 1.575 0 0 1 3.15 0V15a6.15 6.15 0 0 1-6.15 6.15H12A6.15 6.15 0 0 1 5.85 15V9.525"/></svg>',
|
|
7694
|
+
"key": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"/></svg>',
|
|
7695
|
+
"arrow-right": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>',
|
|
7696
|
+
"shield-check": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>',
|
|
7697
|
+
"credit-card": '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>'
|
|
7698
|
+
};
|
|
7699
|
+
function resolveIcon(iconName) {
|
|
7700
|
+
if (!iconName) return "";
|
|
7701
|
+
if (iconName.startsWith("<svg") || iconName.startsWith("<")) return iconName;
|
|
7702
|
+
return ICON_SVG[iconName] || "";
|
|
7703
|
+
}
|
|
7027
7704
|
var MARKER = "<!-- DYNAMIC_PLUGIN_MENU -->";
|
|
7028
7705
|
function renderMenuItem(item, currentPath) {
|
|
7029
7706
|
const isActive = currentPath === item.path || currentPath.startsWith(item.path);
|
|
7030
7707
|
const fallbackIcon = `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>`;
|
|
7708
|
+
const resolvedIcon = resolveIcon(item.icon) || fallbackIcon;
|
|
7031
7709
|
return `
|
|
7032
7710
|
<span class="relative">
|
|
7033
7711
|
${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
|
|
@@ -7037,7 +7715,7 @@ function renderMenuItem(item, currentPath) {
|
|
|
7037
7715
|
${isActive ? 'data-current="true"' : ""}
|
|
7038
7716
|
>
|
|
7039
7717
|
<span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">
|
|
7040
|
-
${
|
|
7718
|
+
${resolvedIcon}
|
|
7041
7719
|
</span>
|
|
7042
7720
|
<span class="truncate">${item.label}</span>
|
|
7043
7721
|
</a>
|
|
@@ -7052,23 +7730,28 @@ function pluginMenuMiddleware() {
|
|
|
7052
7730
|
let activeMenuItems = [];
|
|
7053
7731
|
try {
|
|
7054
7732
|
const db = c.env.DB;
|
|
7055
|
-
const
|
|
7056
|
-
if (
|
|
7057
|
-
const placeholders =
|
|
7733
|
+
const pluginCodeNames = REGISTRY_MENU_PLUGINS.map((p) => p.codeName);
|
|
7734
|
+
if (pluginCodeNames.length > 0) {
|
|
7735
|
+
const placeholders = pluginCodeNames.map(() => "?").join(",");
|
|
7058
7736
|
const result = await db.prepare(
|
|
7059
7737
|
`SELECT name FROM plugins WHERE name IN (${placeholders}) AND status = 'active'`
|
|
7060
|
-
).bind(...
|
|
7738
|
+
).bind(...pluginCodeNames).all();
|
|
7061
7739
|
const activeNames = new Set((result.results || []).map((r) => r.name));
|
|
7062
|
-
for (const plugin2 of
|
|
7063
|
-
if (activeNames.has(plugin2.
|
|
7064
|
-
activeMenuItems.push(
|
|
7740
|
+
for (const plugin2 of REGISTRY_MENU_PLUGINS) {
|
|
7741
|
+
if (activeNames.has(plugin2.codeName)) {
|
|
7742
|
+
activeMenuItems.push({
|
|
7743
|
+
label: plugin2.label,
|
|
7744
|
+
path: plugin2.path,
|
|
7745
|
+
icon: plugin2.icon,
|
|
7746
|
+
order: plugin2.order
|
|
7747
|
+
});
|
|
7065
7748
|
}
|
|
7066
7749
|
}
|
|
7067
|
-
activeMenuItems.sort((a, b) =>
|
|
7750
|
+
activeMenuItems.sort((a, b) => a.order - b.order);
|
|
7068
7751
|
}
|
|
7069
7752
|
} catch {
|
|
7070
7753
|
}
|
|
7071
|
-
c.set("pluginMenuItems", activeMenuItems.map((m) => ({ label: m.label, path: m.path, icon: m.icon || "" })));
|
|
7754
|
+
c.set("pluginMenuItems", activeMenuItems.map((m) => ({ label: m.label, path: m.path, icon: resolveIcon(m.icon) || "" })));
|
|
7072
7755
|
await next();
|
|
7073
7756
|
if (activeMenuItems.length > 0 && c.res.headers.get("content-type")?.includes("text/html")) {
|
|
7074
7757
|
const status = c.res.status;
|
|
@@ -7085,6 +7768,672 @@ function pluginMenuMiddleware() {
|
|
|
7085
7768
|
};
|
|
7086
7769
|
}
|
|
7087
7770
|
|
|
7771
|
+
// src/plugins/types.ts
|
|
7772
|
+
var HOOKS2 = {
|
|
7773
|
+
// Request lifecycle
|
|
7774
|
+
REQUEST_START: "request:start",
|
|
7775
|
+
REQUEST_END: "request:end",
|
|
7776
|
+
USER_LOGIN: "user:login"};
|
|
7777
|
+
init_admin_layout_catalyst_template();
|
|
7778
|
+
var adminRoutes4 = new Hono();
|
|
7779
|
+
adminRoutes4.use("*", requireAuth());
|
|
7780
|
+
adminRoutes4.use("*", async (c, next) => {
|
|
7781
|
+
const user = c.get("user");
|
|
7782
|
+
if (user?.role !== "admin") {
|
|
7783
|
+
return c.text("Access denied", 403);
|
|
7784
|
+
}
|
|
7785
|
+
return next();
|
|
7786
|
+
});
|
|
7787
|
+
adminRoutes4.get("/", async (c) => {
|
|
7788
|
+
const user = c.get("user");
|
|
7789
|
+
const db = c.env.DB;
|
|
7790
|
+
let totalRequests = 0;
|
|
7791
|
+
let uniqueIPs = 0;
|
|
7792
|
+
let avgDuration = 0;
|
|
7793
|
+
let errorCount = 0;
|
|
7794
|
+
let topPages = [];
|
|
7795
|
+
let recentActivity = [];
|
|
7796
|
+
try {
|
|
7797
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7798
|
+
const dayAgo = now - 86400;
|
|
7799
|
+
const [requestsResult, ipsResult, durationResult, errorsResult, pagesResult, activityResult] = await Promise.all([
|
|
7800
|
+
db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
|
|
7801
|
+
db.prepare("SELECT COUNT(DISTINCT ip_address) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
|
|
7802
|
+
db.prepare("SELECT AVG(duration) as avg FROM system_logs WHERE category = ? AND created_at > ? AND duration IS NOT NULL").bind("api", dayAgo).first(),
|
|
7803
|
+
db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE level IN (?, ?) AND created_at > ?").bind("error", "fatal", dayAgo).first(),
|
|
7804
|
+
db.prepare("SELECT url, COUNT(*) as views FROM system_logs WHERE category = ? AND created_at > ? AND url IS NOT NULL GROUP BY url ORDER BY views DESC LIMIT 10").bind("api", dayAgo).all(),
|
|
7805
|
+
db.prepare("SELECT url, method, status_code, duration, created_at FROM system_logs WHERE category = ? ORDER BY created_at DESC LIMIT 20").bind("api").all()
|
|
7806
|
+
]);
|
|
7807
|
+
totalRequests = requestsResult?.count || 0;
|
|
7808
|
+
uniqueIPs = ipsResult?.count || 0;
|
|
7809
|
+
avgDuration = Math.round(durationResult?.avg || 0);
|
|
7810
|
+
errorCount = errorsResult?.count || 0;
|
|
7811
|
+
topPages = (pagesResult.results || []).map((r) => ({ path: r.url, views: r.views }));
|
|
7812
|
+
recentActivity = activityResult.results || [];
|
|
7813
|
+
} catch {
|
|
7814
|
+
}
|
|
7815
|
+
const content2 = `
|
|
7816
|
+
<div class="space-y-8">
|
|
7817
|
+
<div>
|
|
7818
|
+
<h1 class="text-2xl font-semibold text-zinc-950 dark:text-white">Analytics Dashboard</h1>
|
|
7819
|
+
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Last 24 hours overview from system logs</p>
|
|
7820
|
+
</div>
|
|
7821
|
+
|
|
7822
|
+
<!-- Stats Cards -->
|
|
7823
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
7824
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7825
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Total Requests</p>
|
|
7826
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${totalRequests.toLocaleString()}</p>
|
|
7827
|
+
</div>
|
|
7828
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7829
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Unique Visitors</p>
|
|
7830
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${uniqueIPs.toLocaleString()}</p>
|
|
7831
|
+
</div>
|
|
7832
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7833
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Avg Response Time</p>
|
|
7834
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${avgDuration}ms</p>
|
|
7835
|
+
</div>
|
|
7836
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7837
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Errors</p>
|
|
7838
|
+
<p class="mt-2 text-3xl font-semibold ${errorCount > 0 ? "text-red-600 dark:text-red-400" : "text-zinc-950 dark:text-white"}">${errorCount.toLocaleString()}</p>
|
|
7839
|
+
</div>
|
|
7840
|
+
</div>
|
|
7841
|
+
|
|
7842
|
+
<!-- Top Pages -->
|
|
7843
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7844
|
+
<div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
|
|
7845
|
+
<h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Top Pages</h2>
|
|
7846
|
+
</div>
|
|
7847
|
+
<div class="divide-y divide-zinc-950/5 dark:divide-white/10">
|
|
7848
|
+
${topPages.length > 0 ? topPages.map((p) => `
|
|
7849
|
+
<div class="flex items-center justify-between px-6 py-3">
|
|
7850
|
+
<span class="text-sm text-zinc-700 dark:text-zinc-300 font-mono truncate">${escapeHtml3(p.path)}</span>
|
|
7851
|
+
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${p.views}</span>
|
|
7852
|
+
</div>
|
|
7853
|
+
`).join("") : `
|
|
7854
|
+
<div class="px-6 py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
|
7855
|
+
No page views recorded yet. Analytics data will appear once requests are logged.
|
|
7856
|
+
</div>
|
|
7857
|
+
`}
|
|
7858
|
+
</div>
|
|
7859
|
+
</div>
|
|
7860
|
+
|
|
7861
|
+
<!-- Recent Activity -->
|
|
7862
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7863
|
+
<div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
|
|
7864
|
+
<h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Recent Activity</h2>
|
|
7865
|
+
</div>
|
|
7866
|
+
<div class="overflow-x-auto">
|
|
7867
|
+
<table class="w-full text-sm">
|
|
7868
|
+
<thead class="bg-zinc-50 dark:bg-zinc-800/50">
|
|
7869
|
+
<tr>
|
|
7870
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Path</th>
|
|
7871
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Method</th>
|
|
7872
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Status</th>
|
|
7873
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Duration</th>
|
|
7874
|
+
</tr>
|
|
7875
|
+
</thead>
|
|
7876
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/10">
|
|
7877
|
+
${recentActivity.length > 0 ? recentActivity.map((a) => `
|
|
7878
|
+
<tr>
|
|
7879
|
+
<td class="px-6 py-2 font-mono text-zinc-700 dark:text-zinc-300 truncate max-w-xs">${escapeHtml3(a.url || "")}</td>
|
|
7880
|
+
<td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${a.method === "GET" ? "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}">${a.method || ""}</span></td>
|
|
7881
|
+
<td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${(a.status_code || 0) >= 400 ? "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400"}">${a.status_code || ""}</span></td>
|
|
7882
|
+
<td class="px-6 py-2 text-zinc-500 dark:text-zinc-400">${a.duration || 0}ms</td>
|
|
7883
|
+
</tr>
|
|
7884
|
+
`).join("") : `
|
|
7885
|
+
<tr>
|
|
7886
|
+
<td colspan="4" class="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400">No activity recorded yet.</td>
|
|
7887
|
+
</tr>
|
|
7888
|
+
`}
|
|
7889
|
+
</tbody>
|
|
7890
|
+
</table>
|
|
7891
|
+
</div>
|
|
7892
|
+
</div>
|
|
7893
|
+
</div>
|
|
7894
|
+
`;
|
|
7895
|
+
return c.html(renderAdminLayoutCatalyst({
|
|
7896
|
+
title: "Analytics",
|
|
7897
|
+
pageTitle: "Analytics Dashboard",
|
|
7898
|
+
currentPath: "/admin/analytics",
|
|
7899
|
+
version: c.get("appVersion"),
|
|
7900
|
+
user: user ? {
|
|
7901
|
+
name: user.email.split("@")[0] || "Admin",
|
|
7902
|
+
email: user.email,
|
|
7903
|
+
role: user.role
|
|
7904
|
+
} : void 0,
|
|
7905
|
+
content: content2,
|
|
7906
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7907
|
+
}));
|
|
7908
|
+
});
|
|
7909
|
+
function escapeHtml3(str) {
|
|
7910
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7911
|
+
}
|
|
7912
|
+
|
|
7913
|
+
// src/plugins/core-plugins/analytics/index.ts
|
|
7914
|
+
function createAnalyticsPlugin() {
|
|
7915
|
+
const builder = PluginBuilder.create({
|
|
7916
|
+
name: "core-analytics",
|
|
7917
|
+
version: "1.0.0-beta.1",
|
|
7918
|
+
description: "Core analytics tracking and reporting plugin"
|
|
7919
|
+
});
|
|
7920
|
+
builder.metadata({
|
|
7921
|
+
author: {
|
|
7922
|
+
name: "SonicJS Team",
|
|
7923
|
+
email: "team@sonicjs.com"
|
|
7924
|
+
},
|
|
7925
|
+
license: "MIT",
|
|
7926
|
+
compatibility: "^0.1.0",
|
|
7927
|
+
dependencies: ["core-auth"]
|
|
7928
|
+
// Requires auth for admin access
|
|
7929
|
+
});
|
|
7930
|
+
const analyticsAPI = new Hono();
|
|
7931
|
+
analyticsAPI.get("/stats", async (c) => {
|
|
7932
|
+
const timeRange = c.req.query("range") || "7d";
|
|
7933
|
+
c.req.query("metric") || "all";
|
|
7934
|
+
return c.json({
|
|
7935
|
+
message: "Analytics stats",
|
|
7936
|
+
data: {
|
|
7937
|
+
pageviews: 12500,
|
|
7938
|
+
uniqueVisitors: 3200,
|
|
7939
|
+
sessions: 4800,
|
|
7940
|
+
avgSessionDuration: 245,
|
|
7941
|
+
bounceRate: 0.35,
|
|
7942
|
+
topPages: [
|
|
7943
|
+
{ path: "/", views: 3200 },
|
|
7944
|
+
{ path: "/about", views: 1800 },
|
|
7945
|
+
{ path: "/contact", views: 950 }
|
|
7946
|
+
],
|
|
7947
|
+
timeRange
|
|
7948
|
+
}
|
|
7949
|
+
});
|
|
7950
|
+
});
|
|
7951
|
+
analyticsAPI.post("/track", async (c) => {
|
|
7952
|
+
const event = await c.req.json();
|
|
7953
|
+
console.info("Analytics event tracked:", event);
|
|
7954
|
+
return c.json({
|
|
7955
|
+
message: "Event tracked successfully",
|
|
7956
|
+
eventId: `event-${Date.now()}`
|
|
7957
|
+
});
|
|
7958
|
+
});
|
|
7959
|
+
analyticsAPI.get("/reports", async (c) => {
|
|
7960
|
+
const reportType = c.req.query("type") || "traffic";
|
|
7961
|
+
const startDate = c.req.query("start");
|
|
7962
|
+
const endDate = c.req.query("end");
|
|
7963
|
+
return c.json({
|
|
7964
|
+
message: "Analytics report",
|
|
7965
|
+
data: {
|
|
7966
|
+
reportType,
|
|
7967
|
+
dateRange: { start: startDate, end: endDate },
|
|
7968
|
+
data: []
|
|
7969
|
+
}
|
|
7970
|
+
});
|
|
7971
|
+
});
|
|
7972
|
+
analyticsAPI.get("/realtime", async (c) => {
|
|
7973
|
+
return c.json({
|
|
7974
|
+
message: "Real-time analytics",
|
|
7975
|
+
data: {
|
|
7976
|
+
activeUsers: 23,
|
|
7977
|
+
activePages: [
|
|
7978
|
+
{ path: "/", users: 8 },
|
|
7979
|
+
{ path: "/blog", users: 5 },
|
|
7980
|
+
{ path: "/products", users: 4 }
|
|
7981
|
+
],
|
|
7982
|
+
recentEvents: []
|
|
7983
|
+
}
|
|
7984
|
+
});
|
|
7985
|
+
});
|
|
7986
|
+
builder.addRoute("/api/analytics", analyticsAPI, {
|
|
7987
|
+
description: "Analytics tracking and reporting API",
|
|
7988
|
+
requiresAuth: true,
|
|
7989
|
+
roles: ["admin", "analytics:read"],
|
|
7990
|
+
priority: 3
|
|
7991
|
+
});
|
|
7992
|
+
builder.addRoute("/admin/analytics", adminRoutes4, {
|
|
7993
|
+
description: "Analytics admin dashboard",
|
|
7994
|
+
requiresAuth: true,
|
|
7995
|
+
priority: 50
|
|
7996
|
+
});
|
|
7997
|
+
builder.addSingleMiddleware("analytics-tracker", async (c, next) => {
|
|
7998
|
+
const start = Date.now();
|
|
7999
|
+
const path = c.req.path;
|
|
8000
|
+
const method = c.req.method;
|
|
8001
|
+
const userAgent = c.req.header("user-agent");
|
|
8002
|
+
const referer = c.req.header("referer");
|
|
8003
|
+
const ip = c.req.header("CF-Connecting-IP") || c.req.header("x-forwarded-for");
|
|
8004
|
+
await next();
|
|
8005
|
+
const duration = Date.now() - start;
|
|
8006
|
+
const status = c.res.status;
|
|
8007
|
+
const analyticsData = {
|
|
8008
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8009
|
+
path,
|
|
8010
|
+
method,
|
|
8011
|
+
status,
|
|
8012
|
+
duration,
|
|
8013
|
+
userAgent,
|
|
8014
|
+
referer,
|
|
8015
|
+
ip,
|
|
8016
|
+
responseSize: c.res.headers.get("content-length") || 0
|
|
8017
|
+
};
|
|
8018
|
+
console.debug("Analytics tracking:", analyticsData);
|
|
8019
|
+
}, {
|
|
8020
|
+
description: "Track page views and request analytics",
|
|
8021
|
+
global: true,
|
|
8022
|
+
priority: 99
|
|
8023
|
+
// Run last to capture response data
|
|
8024
|
+
});
|
|
8025
|
+
builder.addService("analyticsService", {
|
|
8026
|
+
trackEvent: async (event) => {
|
|
8027
|
+
console.info("Tracking event:", event);
|
|
8028
|
+
return { eventId: `event-${Date.now()}` };
|
|
8029
|
+
},
|
|
8030
|
+
trackPageView: async (data) => {
|
|
8031
|
+
console.info("Tracking pageview:", data.path);
|
|
8032
|
+
return { viewId: `view-${Date.now()}` };
|
|
8033
|
+
},
|
|
8034
|
+
getStats: async (_timeRange) => {
|
|
8035
|
+
return {
|
|
8036
|
+
pageviews: 12500,
|
|
8037
|
+
sessions: 4800,
|
|
8038
|
+
uniqueVisitors: 3200
|
|
8039
|
+
};
|
|
8040
|
+
},
|
|
8041
|
+
generateReport: async (type, options) => {
|
|
8042
|
+
console.info(`Generating ${type} report with options:`, options);
|
|
8043
|
+
return { reportId: `report-${Date.now()}` };
|
|
8044
|
+
}
|
|
8045
|
+
}, {
|
|
8046
|
+
description: "Core analytics tracking service",
|
|
8047
|
+
singleton: true
|
|
8048
|
+
});
|
|
8049
|
+
const pageViewSchema = PluginHelpers.createSchema([
|
|
8050
|
+
{ name: "path", type: "string", optional: false },
|
|
8051
|
+
{ name: "title", type: "string", optional: true },
|
|
8052
|
+
{ name: "referrer", type: "string", optional: true },
|
|
8053
|
+
{ name: "userAgent", type: "string", optional: true },
|
|
8054
|
+
{ name: "ipAddress", type: "string", optional: true },
|
|
8055
|
+
{ name: "sessionId", type: "string", optional: true },
|
|
8056
|
+
{ name: "userId", type: "number", optional: true },
|
|
8057
|
+
{ name: "duration", type: "number", optional: true }
|
|
8058
|
+
]);
|
|
8059
|
+
const eventSchema = PluginHelpers.createSchema([
|
|
8060
|
+
{ name: "eventType", type: "string", optional: false },
|
|
8061
|
+
{ name: "eventName", type: "string", optional: false },
|
|
8062
|
+
{ name: "eventData", type: "object", optional: true },
|
|
8063
|
+
{ name: "path", type: "string", optional: true },
|
|
8064
|
+
{ name: "sessionId", type: "string", optional: true },
|
|
8065
|
+
{ name: "userId", type: "number", optional: true }
|
|
8066
|
+
]);
|
|
8067
|
+
const pageViewMigration = PluginHelpers.createMigration("analytics_pageviews", [
|
|
8068
|
+
{ name: "id", type: "INTEGER", primaryKey: true },
|
|
8069
|
+
{ name: "path", type: "TEXT", nullable: false },
|
|
8070
|
+
{ name: "title", type: "TEXT", nullable: true },
|
|
8071
|
+
{ name: "referrer", type: "TEXT", nullable: true },
|
|
8072
|
+
{ name: "user_agent", type: "TEXT", nullable: true },
|
|
8073
|
+
{ name: "ip_address", type: "TEXT", nullable: true },
|
|
8074
|
+
{ name: "session_id", type: "TEXT", nullable: true },
|
|
8075
|
+
{ name: "user_id", type: "INTEGER", nullable: true },
|
|
8076
|
+
{ name: "duration", type: "INTEGER", nullable: true }
|
|
8077
|
+
]);
|
|
8078
|
+
const eventMigration = PluginHelpers.createMigration("analytics_events", [
|
|
8079
|
+
{ name: "id", type: "INTEGER", primaryKey: true },
|
|
8080
|
+
{ name: "event_type", type: "TEXT", nullable: false },
|
|
8081
|
+
{ name: "event_name", type: "TEXT", nullable: false },
|
|
8082
|
+
{ name: "event_data", type: "TEXT", nullable: true },
|
|
8083
|
+
{ name: "path", type: "TEXT", nullable: true },
|
|
8084
|
+
{ name: "session_id", type: "TEXT", nullable: true },
|
|
8085
|
+
{ name: "user_id", type: "INTEGER", nullable: true }
|
|
8086
|
+
]);
|
|
8087
|
+
builder.addModel("PageView", {
|
|
8088
|
+
tableName: "analytics_pageviews",
|
|
8089
|
+
schema: pageViewSchema,
|
|
8090
|
+
migrations: [pageViewMigration]
|
|
8091
|
+
});
|
|
8092
|
+
builder.addModel("AnalyticsEvent", {
|
|
8093
|
+
tableName: "analytics_events",
|
|
8094
|
+
schema: eventSchema,
|
|
8095
|
+
migrations: [eventMigration]
|
|
8096
|
+
});
|
|
8097
|
+
builder.addHook(HOOKS2.REQUEST_START, async (data, _context) => {
|
|
8098
|
+
data.analytics = {
|
|
8099
|
+
startTime: Date.now(),
|
|
8100
|
+
sessionId: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
8101
|
+
};
|
|
8102
|
+
return data;
|
|
8103
|
+
}, {
|
|
8104
|
+
priority: 1,
|
|
8105
|
+
description: "Initialize analytics tracking for requests"
|
|
8106
|
+
});
|
|
8107
|
+
builder.addHook(HOOKS2.REQUEST_END, async (data, _context) => {
|
|
8108
|
+
if (data.analytics) {
|
|
8109
|
+
const duration = Date.now() - data.analytics.startTime;
|
|
8110
|
+
console.debug(`Request completed in ${duration}ms`);
|
|
8111
|
+
}
|
|
8112
|
+
return data;
|
|
8113
|
+
}, {
|
|
8114
|
+
priority: 1,
|
|
8115
|
+
description: "Complete analytics tracking for requests"
|
|
8116
|
+
});
|
|
8117
|
+
builder.addHook(HOOKS2.USER_LOGIN, async (data, context) => {
|
|
8118
|
+
await context.services?.analyticsService?.trackEvent({
|
|
8119
|
+
eventType: "auth",
|
|
8120
|
+
eventName: "user_login",
|
|
8121
|
+
userId: data.userId,
|
|
8122
|
+
eventData: { loginMethod: data.method }
|
|
8123
|
+
});
|
|
8124
|
+
return data;
|
|
8125
|
+
}, {
|
|
8126
|
+
priority: 8,
|
|
8127
|
+
description: "Track user login events"
|
|
8128
|
+
});
|
|
8129
|
+
builder.addHook("content:view", async (data, context) => {
|
|
8130
|
+
await context.services?.analyticsService?.trackEvent({
|
|
8131
|
+
eventType: "content",
|
|
8132
|
+
eventName: "content_view",
|
|
8133
|
+
eventData: {
|
|
8134
|
+
contentId: data.id,
|
|
8135
|
+
contentType: data.type,
|
|
8136
|
+
title: data.title
|
|
8137
|
+
}
|
|
8138
|
+
});
|
|
8139
|
+
return data;
|
|
8140
|
+
}, {
|
|
8141
|
+
priority: 8,
|
|
8142
|
+
description: "Track content view events"
|
|
8143
|
+
});
|
|
8144
|
+
builder.addAdminPage(
|
|
8145
|
+
"/analytics",
|
|
8146
|
+
"Analytics Dashboard",
|
|
8147
|
+
"AnalyticsDashboardView",
|
|
8148
|
+
{
|
|
8149
|
+
description: "View analytics overview and key metrics",
|
|
8150
|
+
permissions: ["admin", "analytics:read"],
|
|
8151
|
+
icon: "chart-bar"
|
|
8152
|
+
}
|
|
8153
|
+
);
|
|
8154
|
+
builder.addAdminPage(
|
|
8155
|
+
"/analytics/reports",
|
|
8156
|
+
"Analytics Reports",
|
|
8157
|
+
"AnalyticsReportsView",
|
|
8158
|
+
{
|
|
8159
|
+
description: "Generate and view detailed analytics reports",
|
|
8160
|
+
permissions: ["admin", "analytics:read"],
|
|
8161
|
+
icon: "document-report"
|
|
8162
|
+
}
|
|
8163
|
+
);
|
|
8164
|
+
builder.addAdminPage(
|
|
8165
|
+
"/analytics/realtime",
|
|
8166
|
+
"Real-time Analytics",
|
|
8167
|
+
"AnalyticsRealtimeView",
|
|
8168
|
+
{
|
|
8169
|
+
description: "View real-time visitor activity",
|
|
8170
|
+
permissions: ["admin", "analytics:read"],
|
|
8171
|
+
icon: "lightning-bolt"
|
|
8172
|
+
}
|
|
8173
|
+
);
|
|
8174
|
+
builder.addAdminPage(
|
|
8175
|
+
"/analytics/settings",
|
|
8176
|
+
"Analytics Settings",
|
|
8177
|
+
"AnalyticsSettingsView",
|
|
8178
|
+
{
|
|
8179
|
+
description: "Configure analytics tracking and data collection",
|
|
8180
|
+
permissions: ["admin", "analytics:configure"],
|
|
8181
|
+
icon: "cog"
|
|
8182
|
+
}
|
|
8183
|
+
);
|
|
8184
|
+
builder.addMenuItem("Analytics", "/admin/analytics", {
|
|
8185
|
+
icon: "chart-bar",
|
|
8186
|
+
order: 40,
|
|
8187
|
+
permissions: ["admin", "analytics:read"]
|
|
8188
|
+
});
|
|
8189
|
+
builder.addMenuItem("Dashboard", "/admin/analytics", {
|
|
8190
|
+
icon: "chart-bar",
|
|
8191
|
+
parent: "Analytics",
|
|
8192
|
+
order: 1,
|
|
8193
|
+
permissions: ["admin", "analytics:read"]
|
|
8194
|
+
});
|
|
8195
|
+
builder.addMenuItem("Reports", "/admin/analytics/reports", {
|
|
8196
|
+
icon: "document-report",
|
|
8197
|
+
parent: "Analytics",
|
|
8198
|
+
order: 2,
|
|
8199
|
+
permissions: ["admin", "analytics:read"]
|
|
8200
|
+
});
|
|
8201
|
+
builder.addMenuItem("Real-time", "/admin/analytics/realtime", {
|
|
8202
|
+
icon: "lightning-bolt",
|
|
8203
|
+
parent: "Analytics",
|
|
8204
|
+
order: 3,
|
|
8205
|
+
permissions: ["admin", "analytics:read"]
|
|
8206
|
+
});
|
|
8207
|
+
builder.addMenuItem("Settings", "/admin/analytics/settings", {
|
|
8208
|
+
icon: "cog",
|
|
8209
|
+
parent: "Analytics",
|
|
8210
|
+
order: 4,
|
|
8211
|
+
permissions: ["admin", "analytics:configure"]
|
|
8212
|
+
});
|
|
8213
|
+
builder.lifecycle({
|
|
8214
|
+
install: async () => {
|
|
8215
|
+
console.info("Installing analytics plugin...");
|
|
8216
|
+
},
|
|
8217
|
+
activate: async () => {
|
|
8218
|
+
console.info("Activating analytics plugin...");
|
|
8219
|
+
},
|
|
8220
|
+
deactivate: async () => {
|
|
8221
|
+
console.info("Deactivating analytics plugin...");
|
|
8222
|
+
},
|
|
8223
|
+
configure: async (config) => {
|
|
8224
|
+
console.info("Configuring analytics plugin...", config);
|
|
8225
|
+
}
|
|
8226
|
+
});
|
|
8227
|
+
return builder.build();
|
|
8228
|
+
}
|
|
8229
|
+
var analyticsPlugin = createAnalyticsPlugin();
|
|
8230
|
+
|
|
8231
|
+
// src/plugins/core-plugins/analytics/services/event-tracking-service.ts
|
|
8232
|
+
var EventTrackingService = class {
|
|
8233
|
+
constructor(db) {
|
|
8234
|
+
this.db = db;
|
|
8235
|
+
}
|
|
8236
|
+
async trackEvent(input) {
|
|
8237
|
+
const id = crypto.randomUUID();
|
|
8238
|
+
const category = input.category || "user-activity";
|
|
8239
|
+
await this.db.prepare(`
|
|
8240
|
+
INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
|
|
8241
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8242
|
+
`).bind(
|
|
8243
|
+
id,
|
|
8244
|
+
input.event,
|
|
8245
|
+
category,
|
|
8246
|
+
input.properties ? JSON.stringify(input.properties) : null,
|
|
8247
|
+
input.userId || null,
|
|
8248
|
+
input.sessionId || null,
|
|
8249
|
+
input.ipAddress || null,
|
|
8250
|
+
input.userAgent || null,
|
|
8251
|
+
input.path || null
|
|
8252
|
+
).run();
|
|
8253
|
+
return id;
|
|
8254
|
+
}
|
|
8255
|
+
async trackBatch(events) {
|
|
8256
|
+
const ids = [];
|
|
8257
|
+
const stmts = events.map((input) => {
|
|
8258
|
+
const id = crypto.randomUUID();
|
|
8259
|
+
ids.push(id);
|
|
8260
|
+
const category = input.category || "user-activity";
|
|
8261
|
+
return this.db.prepare(`
|
|
8262
|
+
INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
|
|
8263
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8264
|
+
`).bind(
|
|
8265
|
+
id,
|
|
8266
|
+
input.event,
|
|
8267
|
+
category,
|
|
8268
|
+
input.properties ? JSON.stringify(input.properties) : null,
|
|
8269
|
+
input.userId || null,
|
|
8270
|
+
input.sessionId || null,
|
|
8271
|
+
input.ipAddress || null,
|
|
8272
|
+
input.userAgent || null,
|
|
8273
|
+
input.path || null
|
|
8274
|
+
);
|
|
8275
|
+
});
|
|
8276
|
+
await this.db.batch(stmts);
|
|
8277
|
+
return ids;
|
|
8278
|
+
}
|
|
8279
|
+
async queryEvents(filters = {}) {
|
|
8280
|
+
const conditions = [];
|
|
8281
|
+
const params = [];
|
|
8282
|
+
if (filters.event) {
|
|
8283
|
+
conditions.push("event = ?");
|
|
8284
|
+
params.push(filters.event);
|
|
8285
|
+
}
|
|
8286
|
+
if (filters.category) {
|
|
8287
|
+
conditions.push("category = ?");
|
|
8288
|
+
params.push(filters.category);
|
|
8289
|
+
}
|
|
8290
|
+
if (filters.userId) {
|
|
8291
|
+
conditions.push("user_id = ?");
|
|
8292
|
+
params.push(filters.userId);
|
|
8293
|
+
}
|
|
8294
|
+
if (filters.sessionId) {
|
|
8295
|
+
conditions.push("session_id = ?");
|
|
8296
|
+
params.push(filters.sessionId);
|
|
8297
|
+
}
|
|
8298
|
+
if (filters.startDate) {
|
|
8299
|
+
conditions.push("created_at >= ?");
|
|
8300
|
+
params.push(filters.startDate);
|
|
8301
|
+
}
|
|
8302
|
+
if (filters.endDate) {
|
|
8303
|
+
conditions.push("created_at <= ?");
|
|
8304
|
+
params.push(filters.endDate);
|
|
8305
|
+
}
|
|
8306
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8307
|
+
const limit = filters.limit || 50;
|
|
8308
|
+
const offset = filters.offset || 0;
|
|
8309
|
+
const [countResult, eventsResult] = await Promise.all([
|
|
8310
|
+
this.db.prepare(`SELECT COUNT(*) as total FROM analytics_events ${where}`).bind(...params).first(),
|
|
8311
|
+
this.db.prepare(`SELECT * FROM analytics_events ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).bind(...params, limit, offset).all()
|
|
8312
|
+
]);
|
|
8313
|
+
const events = (eventsResult.results || []).map((e) => ({
|
|
8314
|
+
...e,
|
|
8315
|
+
properties: e.properties ? JSON.parse(e.properties) : null
|
|
8316
|
+
}));
|
|
8317
|
+
return { events, total: countResult?.total || 0 };
|
|
8318
|
+
}
|
|
8319
|
+
async getStats(startDate, endDate) {
|
|
8320
|
+
const conditions = [];
|
|
8321
|
+
const params = [];
|
|
8322
|
+
if (startDate) {
|
|
8323
|
+
conditions.push("created_at >= ?");
|
|
8324
|
+
params.push(startDate);
|
|
8325
|
+
}
|
|
8326
|
+
if (endDate) {
|
|
8327
|
+
conditions.push("created_at <= ?");
|
|
8328
|
+
params.push(endDate);
|
|
8329
|
+
}
|
|
8330
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8331
|
+
const [totals, topEvents] = await Promise.all([
|
|
8332
|
+
this.db.prepare(`
|
|
8333
|
+
SELECT
|
|
8334
|
+
COUNT(*) as total_events,
|
|
8335
|
+
COUNT(DISTINCT user_id) as unique_users,
|
|
8336
|
+
COUNT(DISTINCT session_id) as unique_sessions
|
|
8337
|
+
FROM analytics_events ${where}
|
|
8338
|
+
`).bind(...params).first(),
|
|
8339
|
+
this.db.prepare(`
|
|
8340
|
+
SELECT event, COUNT(*) as count
|
|
8341
|
+
FROM analytics_events ${where}
|
|
8342
|
+
GROUP BY event ORDER BY count DESC LIMIT 20
|
|
8343
|
+
`).bind(...params).all()
|
|
8344
|
+
]);
|
|
8345
|
+
return {
|
|
8346
|
+
totalEvents: totals?.total_events || 0,
|
|
8347
|
+
uniqueUsers: totals?.unique_users || 0,
|
|
8348
|
+
uniqueSessions: totals?.unique_sessions || 0,
|
|
8349
|
+
topEvents: (topEvents.results || []).map((r) => ({ event: r.event, count: r.count }))
|
|
8350
|
+
};
|
|
8351
|
+
}
|
|
8352
|
+
};
|
|
8353
|
+
|
|
8354
|
+
// src/plugins/core-plugins/analytics/routes/api.ts
|
|
8355
|
+
var apiRoutes4 = new Hono();
|
|
8356
|
+
apiRoutes4.post("/", async (c) => {
|
|
8357
|
+
const db = c.env.DB;
|
|
8358
|
+
const service = new EventTrackingService(db);
|
|
8359
|
+
const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
8360
|
+
const userAgent = c.req.header("user-agent") || "";
|
|
8361
|
+
const user = c.get("user");
|
|
8362
|
+
let body;
|
|
8363
|
+
try {
|
|
8364
|
+
body = await c.req.json();
|
|
8365
|
+
} catch {
|
|
8366
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8367
|
+
}
|
|
8368
|
+
if (Array.isArray(body)) {
|
|
8369
|
+
if (body.length > 100) {
|
|
8370
|
+
return c.json({ error: "Batch size limit is 100 events" }, 400);
|
|
8371
|
+
}
|
|
8372
|
+
const events = body.map((e) => ({
|
|
8373
|
+
event: e.event,
|
|
8374
|
+
category: e.category || "user-activity",
|
|
8375
|
+
properties: e.properties,
|
|
8376
|
+
userId: user?.userId || e.userId,
|
|
8377
|
+
sessionId: e.sessionId,
|
|
8378
|
+
ipAddress: ip,
|
|
8379
|
+
userAgent,
|
|
8380
|
+
path: e.path
|
|
8381
|
+
}));
|
|
8382
|
+
const invalid = events.find((e) => !e.event || typeof e.event !== "string");
|
|
8383
|
+
if (invalid) {
|
|
8384
|
+
return c.json({ error: 'Each event must have an "event" string field' }, 400);
|
|
8385
|
+
}
|
|
8386
|
+
const ids = await service.trackBatch(events);
|
|
8387
|
+
return c.json({ success: true, eventIds: ids, count: ids.length });
|
|
8388
|
+
}
|
|
8389
|
+
if (!body.event || typeof body.event !== "string") {
|
|
8390
|
+
return c.json({ error: '"event" field is required and must be a string' }, 400);
|
|
8391
|
+
}
|
|
8392
|
+
const eventId = await service.trackEvent({
|
|
8393
|
+
event: body.event,
|
|
8394
|
+
category: body.category || "user-activity",
|
|
8395
|
+
properties: body.properties,
|
|
8396
|
+
userId: user?.userId || body.userId,
|
|
8397
|
+
sessionId: body.sessionId,
|
|
8398
|
+
ipAddress: ip,
|
|
8399
|
+
userAgent,
|
|
8400
|
+
path: body.path
|
|
8401
|
+
});
|
|
8402
|
+
return c.json({ success: true, eventId });
|
|
8403
|
+
});
|
|
8404
|
+
apiRoutes4.get("/", async (c) => {
|
|
8405
|
+
const user = c.get("user");
|
|
8406
|
+
if (!user || user.role !== "admin") {
|
|
8407
|
+
return c.json({ error: "Admin access required" }, 403);
|
|
8408
|
+
}
|
|
8409
|
+
const db = c.env.DB;
|
|
8410
|
+
const service = new EventTrackingService(db);
|
|
8411
|
+
const filters = {
|
|
8412
|
+
event: c.req.query("event") || void 0,
|
|
8413
|
+
category: c.req.query("category") || void 0,
|
|
8414
|
+
userId: c.req.query("userId") || void 0,
|
|
8415
|
+
sessionId: c.req.query("sessionId") || void 0,
|
|
8416
|
+
startDate: c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0,
|
|
8417
|
+
endDate: c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0,
|
|
8418
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
|
|
8419
|
+
offset: c.req.query("offset") ? parseInt(c.req.query("offset")) : 0
|
|
8420
|
+
};
|
|
8421
|
+
const result = await service.queryEvents(filters);
|
|
8422
|
+
return c.json(result);
|
|
8423
|
+
});
|
|
8424
|
+
apiRoutes4.get("/stats", async (c) => {
|
|
8425
|
+
const user = c.get("user");
|
|
8426
|
+
if (!user || user.role !== "admin") {
|
|
8427
|
+
return c.json({ error: "Admin access required" }, 403);
|
|
8428
|
+
}
|
|
8429
|
+
const db = c.env.DB;
|
|
8430
|
+
const service = new EventTrackingService(db);
|
|
8431
|
+
const startDate = c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0;
|
|
8432
|
+
const endDate = c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0;
|
|
8433
|
+
const stats = await service.getStats(startDate, endDate);
|
|
8434
|
+
return c.json(stats);
|
|
8435
|
+
});
|
|
8436
|
+
|
|
7088
8437
|
// src/plugins/cache/services/cache-config.ts
|
|
7089
8438
|
var CACHE_CONFIGS = {
|
|
7090
8439
|
// Content (high read, low write)
|
|
@@ -9000,16 +10349,22 @@ function createSonicJSApp(config = {}) {
|
|
|
9000
10349
|
app2.route(route.path, route.handler);
|
|
9001
10350
|
}
|
|
9002
10351
|
}
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
10352
|
+
if (analyticsPlugin.routes && analyticsPlugin.routes.length > 0) {
|
|
10353
|
+
for (const route of analyticsPlugin.routes) {
|
|
10354
|
+
app2.route(route.path, route.handler);
|
|
10355
|
+
}
|
|
10356
|
+
}
|
|
10357
|
+
app2.route("/api/events", apiRoutes4);
|
|
9008
10358
|
if (stripePlugin.routes && stripePlugin.routes.length > 0) {
|
|
9009
10359
|
for (const route of stripePlugin.routes) {
|
|
9010
10360
|
app2.route(route.path, route.handler);
|
|
9011
10361
|
}
|
|
9012
10362
|
}
|
|
10363
|
+
app2.route("/admin/plugins", adminPluginRoutes);
|
|
10364
|
+
app2.route("/admin/logs", adminLogsRoutes);
|
|
10365
|
+
app2.route("/admin", userRoutes);
|
|
10366
|
+
app2.route("/auth", auth_default);
|
|
10367
|
+
app2.route("/", test_cleanup_default);
|
|
9013
10368
|
if (emailPlugin.routes && emailPlugin.routes.length > 0) {
|
|
9014
10369
|
for (const route of emailPlugin.routes) {
|
|
9015
10370
|
app2.route(route.path, route.handler);
|