@sonicjs-cms/core 2.13.0 → 2.15.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-HVTSE2SF.cjs → chunk-26HYU7MX.cjs} +233 -660
- package/dist/chunk-26HYU7MX.cjs.map +1 -0
- package/dist/{chunk-DB2GJJTM.js → chunk-2BL2A62D.js} +4 -4
- package/dist/{chunk-DB2GJJTM.js.map → chunk-2BL2A62D.js.map} +1 -1
- package/dist/{chunk-I6FFGQIT.cjs → chunk-43AB4EH4.cjs} +723 -211
- package/dist/chunk-43AB4EH4.cjs.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-3VAKUFNQ.js → chunk-5SOFMH66.js} +22 -5
- package/dist/chunk-5SOFMH66.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-IYFSNRZN.js → chunk-7MMD5WMK.js} +49 -476
- package/dist/chunk-7MMD5WMK.js.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-CO4B5EYF.js → chunk-EWXV2KG2.js} +3 -3
- package/dist/{chunk-CO4B5EYF.js.map → chunk-EWXV2KG2.js.map} +1 -1
- package/dist/{chunk-J5WGMRSU.js → chunk-EXNEW5US.js} +76 -17
- package/dist/chunk-EXNEW5US.js.map +1 -0
- package/dist/{chunk-H3XXBAMO.js → chunk-G7XSN72O.js} +722 -212
- package/dist/chunk-G7XSN72O.js.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-SER23XI4.cjs → chunk-RVD7PLMU.cjs} +22 -5
- package/dist/chunk-RVD7PLMU.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-EGUDIX6Q.cjs → chunk-UFPT5KCQ.cjs} +8 -8
- package/dist/{chunk-EGUDIX6Q.cjs.map → chunk-UFPT5KCQ.cjs.map} +1 -1
- package/dist/chunk-UYJ6TJHX.cjs +691 -0
- package/dist/chunk-UYJ6TJHX.cjs.map +1 -0
- package/dist/{chunk-XCP5GCBE.cjs → chunk-VUISYUHY.cjs} +3 -3
- package/dist/{chunk-XCP5GCBE.cjs.map → chunk-VUISYUHY.cjs.map} +1 -1
- package/dist/{chunk-JTUCC6WZ.js → chunk-XWIA3HVX.js} +9 -683
- package/dist/chunk-XWIA3HVX.js.map +1 -0
- package/dist/index.cjs +1604 -173
- 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 +1461 -30
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +29 -29
- package/dist/middleware.js +3 -3
- package/dist/migrations-APFGYCB6.cjs +13 -0
- package/dist/{migrations-GMHTJI7D.cjs.map → migrations-APFGYCB6.cjs.map} +1 -1
- package/dist/migrations-YB52SLW7.js +4 -0
- package/dist/{migrations-IVFIDOSO.js.map → migrations-YB52SLW7.js.map} +1 -1
- package/dist/{plugin-bootstrap-DVGLQrcO.d.cts → plugin-bootstrap-DfVerYV4.d.cts} +3 -1
- package/dist/{plugin-bootstrap-CZ1GDum7.d.ts → plugin-bootstrap-P_ciLp_C.d.ts} +3 -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/package.json +1 -1
- package/dist/chunk-3VAKUFNQ.js.map +0 -1
- package/dist/chunk-6FHNRRJ3.cjs.map +0 -1
- package/dist/chunk-H3XXBAMO.js.map +0 -1
- package/dist/chunk-HVTSE2SF.cjs.map +0 -1
- package/dist/chunk-I6FFGQIT.cjs.map +0 -1
- package/dist/chunk-IYFSNRZN.js.map +0 -1
- package/dist/chunk-J5WGMRSU.js.map +0 -1
- package/dist/chunk-JTUCC6WZ.js.map +0 -1
- package/dist/chunk-QP3OHHON.cjs.map +0 -1
- package/dist/chunk-SER23XI4.cjs.map +0 -1
- package/dist/migrations-GMHTJI7D.cjs +0 -13
- package/dist/migrations-IVFIDOSO.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-7MMD5WMK.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-7MMD5WMK.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-2BL2A62D.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-2BL2A62D.js';
|
|
7
|
+
import { PluginService, PLUGIN_REGISTRY } from './chunk-G7XSN72O.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-G7XSN72O.js';
|
|
9
|
+
export { MigrationService } from './chunk-5SOFMH66.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 } from './chunk-EXNEW5US.js';
|
|
16
|
+
export { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
|
|
17
|
+
import { package_default, getCoreVersion } from './chunk-EWXV2KG2.js';
|
|
18
|
+
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-EWXV2KG2.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';
|
|
@@ -3901,7 +3902,7 @@ function renderSettingsPage(data) {
|
|
|
3901
3902
|
const indexStatusMap = data.indexStatus || {};
|
|
3902
3903
|
const status = indexStatusMap[collectionId];
|
|
3903
3904
|
const isNew = collection.is_new === true && !isDismissed && !status;
|
|
3904
|
-
const
|
|
3905
|
+
const statusBadge2 = status && isChecked ? `<span class="ml-2 px-2 py-1 text-xs rounded-full ${status.status === "completed" ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300" : status.status === "indexing" ? "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" : status.status === "error" ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300" : "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"}">${status.status}</span>` : "";
|
|
3905
3906
|
return `<div class="flex items-start gap-3 p-3 rounded-lg border border-zinc-200 dark:border-zinc-700 ${isNew ? "bg-blue-50 dark:bg-blue-900/10 border-blue-200 dark:border-blue-800" : "hover:bg-zinc-50 dark:hover:bg-zinc-800"}">
|
|
3906
3907
|
<input
|
|
3907
3908
|
type="checkbox"
|
|
@@ -3916,7 +3917,7 @@ function renderSettingsPage(data) {
|
|
|
3916
3917
|
<label for="collection_${collectionId}" class="text-sm font-medium text-zinc-950 dark:text-white select-none cursor-pointer flex items-center">
|
|
3917
3918
|
${collection.display_name || collection.name || "Unnamed Collection"}
|
|
3918
3919
|
${isNew ? '<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">NEW</span>' : ""}
|
|
3919
|
-
${
|
|
3920
|
+
${statusBadge2}
|
|
3920
3921
|
</label>
|
|
3921
3922
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
|
3922
3923
|
${collection.description || collection.name || "No description"} \u2022 ${collection.item_count || 0} items
|
|
@@ -6271,14 +6272,1434 @@ function createSecurityAuditPlugin() {
|
|
|
6271
6272
|
}
|
|
6272
6273
|
var securityAuditPlugin = createSecurityAuditPlugin();
|
|
6273
6274
|
|
|
6275
|
+
// src/plugins/core-plugins/stripe-plugin/services/subscription-service.ts
|
|
6276
|
+
var SubscriptionService = class {
|
|
6277
|
+
constructor(db) {
|
|
6278
|
+
this.db = db;
|
|
6279
|
+
}
|
|
6280
|
+
/**
|
|
6281
|
+
* Ensure the subscriptions table exists
|
|
6282
|
+
*/
|
|
6283
|
+
async ensureTable() {
|
|
6284
|
+
await this.db.prepare(`
|
|
6285
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
6286
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
6287
|
+
user_id TEXT NOT NULL,
|
|
6288
|
+
stripe_customer_id TEXT NOT NULL,
|
|
6289
|
+
stripe_subscription_id TEXT NOT NULL UNIQUE,
|
|
6290
|
+
stripe_price_id TEXT NOT NULL,
|
|
6291
|
+
status TEXT NOT NULL DEFAULT 'incomplete',
|
|
6292
|
+
current_period_start INTEGER NOT NULL DEFAULT 0,
|
|
6293
|
+
current_period_end INTEGER NOT NULL DEFAULT 0,
|
|
6294
|
+
cancel_at_period_end INTEGER NOT NULL DEFAULT 0,
|
|
6295
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
6296
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
6297
|
+
)
|
|
6298
|
+
`).run();
|
|
6299
|
+
await this.db.prepare(`
|
|
6300
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id)
|
|
6301
|
+
`).run();
|
|
6302
|
+
await this.db.prepare(`
|
|
6303
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id)
|
|
6304
|
+
`).run();
|
|
6305
|
+
await this.db.prepare(`
|
|
6306
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id)
|
|
6307
|
+
`).run();
|
|
6308
|
+
await this.db.prepare(`
|
|
6309
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status)
|
|
6310
|
+
`).run();
|
|
6311
|
+
}
|
|
6312
|
+
/**
|
|
6313
|
+
* Create a new subscription record
|
|
6314
|
+
*/
|
|
6315
|
+
async create(data) {
|
|
6316
|
+
const result = await this.db.prepare(`
|
|
6317
|
+
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)
|
|
6318
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6319
|
+
RETURNING *
|
|
6320
|
+
`).bind(
|
|
6321
|
+
data.userId,
|
|
6322
|
+
data.stripeCustomerId,
|
|
6323
|
+
data.stripeSubscriptionId,
|
|
6324
|
+
data.stripePriceId,
|
|
6325
|
+
data.status,
|
|
6326
|
+
data.currentPeriodStart,
|
|
6327
|
+
data.currentPeriodEnd,
|
|
6328
|
+
data.cancelAtPeriodEnd ? 1 : 0
|
|
6329
|
+
).first();
|
|
6330
|
+
return this.mapRow(result);
|
|
6331
|
+
}
|
|
6332
|
+
/**
|
|
6333
|
+
* Upsert a subscription by stripe_subscription_id (INSERT or UPDATE on conflict)
|
|
6334
|
+
*/
|
|
6335
|
+
async upsert(data) {
|
|
6336
|
+
const result = await this.db.prepare(`
|
|
6337
|
+
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)
|
|
6338
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6339
|
+
ON CONFLICT(stripe_subscription_id) DO UPDATE SET
|
|
6340
|
+
status = excluded.status,
|
|
6341
|
+
stripe_price_id = excluded.stripe_price_id,
|
|
6342
|
+
current_period_start = excluded.current_period_start,
|
|
6343
|
+
current_period_end = excluded.current_period_end,
|
|
6344
|
+
cancel_at_period_end = excluded.cancel_at_period_end,
|
|
6345
|
+
updated_at = unixepoch()
|
|
6346
|
+
RETURNING *
|
|
6347
|
+
`).bind(
|
|
6348
|
+
data.userId,
|
|
6349
|
+
data.stripeCustomerId,
|
|
6350
|
+
data.stripeSubscriptionId,
|
|
6351
|
+
data.stripePriceId,
|
|
6352
|
+
data.status,
|
|
6353
|
+
data.currentPeriodStart,
|
|
6354
|
+
data.currentPeriodEnd,
|
|
6355
|
+
data.cancelAtPeriodEnd ? 1 : 0
|
|
6356
|
+
).first();
|
|
6357
|
+
return this.mapRow(result);
|
|
6358
|
+
}
|
|
6359
|
+
/**
|
|
6360
|
+
* Update a subscription by its Stripe subscription ID
|
|
6361
|
+
*/
|
|
6362
|
+
async updateByStripeId(stripeSubscriptionId, data) {
|
|
6363
|
+
const sets = [];
|
|
6364
|
+
const values = [];
|
|
6365
|
+
if (data.status !== void 0) {
|
|
6366
|
+
sets.push("status = ?");
|
|
6367
|
+
values.push(data.status);
|
|
6368
|
+
}
|
|
6369
|
+
if (data.stripePriceId !== void 0) {
|
|
6370
|
+
sets.push("stripe_price_id = ?");
|
|
6371
|
+
values.push(data.stripePriceId);
|
|
6372
|
+
}
|
|
6373
|
+
if (data.currentPeriodStart !== void 0) {
|
|
6374
|
+
sets.push("current_period_start = ?");
|
|
6375
|
+
values.push(data.currentPeriodStart);
|
|
6376
|
+
}
|
|
6377
|
+
if (data.currentPeriodEnd !== void 0) {
|
|
6378
|
+
sets.push("current_period_end = ?");
|
|
6379
|
+
values.push(data.currentPeriodEnd);
|
|
6380
|
+
}
|
|
6381
|
+
if (data.cancelAtPeriodEnd !== void 0) {
|
|
6382
|
+
sets.push("cancel_at_period_end = ?");
|
|
6383
|
+
values.push(data.cancelAtPeriodEnd ? 1 : 0);
|
|
6384
|
+
}
|
|
6385
|
+
if (sets.length === 0) return this.getByStripeSubscriptionId(stripeSubscriptionId);
|
|
6386
|
+
sets.push("updated_at = unixepoch()");
|
|
6387
|
+
values.push(stripeSubscriptionId);
|
|
6388
|
+
const result = await this.db.prepare(`
|
|
6389
|
+
UPDATE subscriptions SET ${sets.join(", ")} WHERE stripe_subscription_id = ? RETURNING *
|
|
6390
|
+
`).bind(...values).first();
|
|
6391
|
+
return result ? this.mapRow(result) : null;
|
|
6392
|
+
}
|
|
6393
|
+
/**
|
|
6394
|
+
* Get subscription by Stripe subscription ID
|
|
6395
|
+
*/
|
|
6396
|
+
async getByStripeSubscriptionId(stripeSubscriptionId) {
|
|
6397
|
+
const result = await this.db.prepare(
|
|
6398
|
+
"SELECT * FROM subscriptions WHERE stripe_subscription_id = ?"
|
|
6399
|
+
).bind(stripeSubscriptionId).first();
|
|
6400
|
+
return result ? this.mapRow(result) : null;
|
|
6401
|
+
}
|
|
6402
|
+
/**
|
|
6403
|
+
* Get the active subscription for a user
|
|
6404
|
+
*/
|
|
6405
|
+
async getByUserId(userId) {
|
|
6406
|
+
const result = await this.db.prepare(
|
|
6407
|
+
"SELECT * FROM subscriptions WHERE user_id = ? ORDER BY CASE WHEN status = 'active' THEN 0 WHEN status = 'trialing' THEN 1 ELSE 2 END, updated_at DESC LIMIT 1"
|
|
6408
|
+
).bind(userId).first();
|
|
6409
|
+
return result ? this.mapRow(result) : null;
|
|
6410
|
+
}
|
|
6411
|
+
/**
|
|
6412
|
+
* Get subscription by Stripe customer ID
|
|
6413
|
+
*/
|
|
6414
|
+
async getByStripeCustomerId(stripeCustomerId) {
|
|
6415
|
+
const result = await this.db.prepare(
|
|
6416
|
+
"SELECT * FROM subscriptions WHERE stripe_customer_id = ? ORDER BY updated_at DESC LIMIT 1"
|
|
6417
|
+
).bind(stripeCustomerId).first();
|
|
6418
|
+
return result ? this.mapRow(result) : null;
|
|
6419
|
+
}
|
|
6420
|
+
/**
|
|
6421
|
+
* Find the userId linked to a Stripe customer ID
|
|
6422
|
+
*/
|
|
6423
|
+
async getUserIdByStripeCustomer(stripeCustomerId) {
|
|
6424
|
+
const result = await this.db.prepare(
|
|
6425
|
+
"SELECT user_id FROM subscriptions WHERE stripe_customer_id = ? LIMIT 1"
|
|
6426
|
+
).bind(stripeCustomerId).first();
|
|
6427
|
+
return result?.user_id ?? null;
|
|
6428
|
+
}
|
|
6429
|
+
/**
|
|
6430
|
+
* List subscriptions with filters and pagination
|
|
6431
|
+
*/
|
|
6432
|
+
async list(filters = {}) {
|
|
6433
|
+
const where = [];
|
|
6434
|
+
const values = [];
|
|
6435
|
+
if (filters.status) {
|
|
6436
|
+
where.push("status = ?");
|
|
6437
|
+
values.push(filters.status);
|
|
6438
|
+
}
|
|
6439
|
+
if (filters.userId) {
|
|
6440
|
+
where.push("user_id = ?");
|
|
6441
|
+
values.push(filters.userId);
|
|
6442
|
+
}
|
|
6443
|
+
if (filters.stripeCustomerId) {
|
|
6444
|
+
where.push("stripe_customer_id = ?");
|
|
6445
|
+
values.push(filters.stripeCustomerId);
|
|
6446
|
+
}
|
|
6447
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
6448
|
+
const sortBy = filters.sortBy || "created_at";
|
|
6449
|
+
const sortOrder = filters.sortOrder || "desc";
|
|
6450
|
+
const limit = Math.min(filters.limit || 50, 100);
|
|
6451
|
+
const page = filters.page || 1;
|
|
6452
|
+
const offset = (page - 1) * limit;
|
|
6453
|
+
const countResult = await this.db.prepare(
|
|
6454
|
+
`SELECT COUNT(*) as count FROM subscriptions ${whereClause}`
|
|
6455
|
+
).bind(...values).first();
|
|
6456
|
+
const results = await this.db.prepare(
|
|
6457
|
+
`SELECT s.*, u.email as user_email FROM subscriptions s LEFT JOIN users u ON s.user_id = u.id ${whereClause} ORDER BY ${sortBy} ${sortOrder} LIMIT ? OFFSET ?`
|
|
6458
|
+
).bind(...values, limit, offset).all();
|
|
6459
|
+
return {
|
|
6460
|
+
subscriptions: (results.results || []).map((r) => this.mapRow(r)),
|
|
6461
|
+
total: countResult?.count || 0
|
|
6462
|
+
};
|
|
6463
|
+
}
|
|
6464
|
+
/**
|
|
6465
|
+
* Get subscription stats
|
|
6466
|
+
*/
|
|
6467
|
+
async getStats() {
|
|
6468
|
+
const result = await this.db.prepare(`
|
|
6469
|
+
SELECT
|
|
6470
|
+
COUNT(*) as total,
|
|
6471
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
6472
|
+
SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) as canceled,
|
|
6473
|
+
SUM(CASE WHEN status = 'past_due' THEN 1 ELSE 0 END) as past_due,
|
|
6474
|
+
SUM(CASE WHEN status = 'trialing' THEN 1 ELSE 0 END) as trialing
|
|
6475
|
+
FROM subscriptions
|
|
6476
|
+
`).first();
|
|
6477
|
+
return {
|
|
6478
|
+
total: result?.total || 0,
|
|
6479
|
+
active: result?.active || 0,
|
|
6480
|
+
canceled: result?.canceled || 0,
|
|
6481
|
+
pastDue: result?.past_due || 0,
|
|
6482
|
+
trialing: result?.trialing || 0
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
/**
|
|
6486
|
+
* Delete a subscription record by Stripe subscription ID
|
|
6487
|
+
*/
|
|
6488
|
+
async deleteByStripeId(stripeSubscriptionId) {
|
|
6489
|
+
const result = await this.db.prepare(
|
|
6490
|
+
"DELETE FROM subscriptions WHERE stripe_subscription_id = ?"
|
|
6491
|
+
).bind(stripeSubscriptionId).run();
|
|
6492
|
+
return (result.meta?.changes || 0) > 0;
|
|
6493
|
+
}
|
|
6494
|
+
mapRow(row) {
|
|
6495
|
+
return {
|
|
6496
|
+
id: row.id,
|
|
6497
|
+
userId: row.user_id,
|
|
6498
|
+
stripeCustomerId: row.stripe_customer_id,
|
|
6499
|
+
stripeSubscriptionId: row.stripe_subscription_id,
|
|
6500
|
+
stripePriceId: row.stripe_price_id,
|
|
6501
|
+
status: row.status,
|
|
6502
|
+
currentPeriodStart: row.current_period_start,
|
|
6503
|
+
currentPeriodEnd: row.current_period_end,
|
|
6504
|
+
cancelAtPeriodEnd: !!row.cancel_at_period_end,
|
|
6505
|
+
createdAt: row.created_at,
|
|
6506
|
+
updatedAt: row.updated_at,
|
|
6507
|
+
// Attach email if joined
|
|
6508
|
+
...row.user_email ? { userEmail: row.user_email } : {}
|
|
6509
|
+
};
|
|
6510
|
+
}
|
|
6511
|
+
};
|
|
6512
|
+
|
|
6513
|
+
// src/plugins/core-plugins/stripe-plugin/services/stripe-event-service.ts
|
|
6514
|
+
var StripeEventService = class {
|
|
6515
|
+
constructor(db) {
|
|
6516
|
+
this.db = db;
|
|
6517
|
+
}
|
|
6518
|
+
async ensureTable() {
|
|
6519
|
+
await this.db.prepare(`
|
|
6520
|
+
CREATE TABLE IF NOT EXISTS stripe_events (
|
|
6521
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
6522
|
+
stripe_event_id TEXT NOT NULL UNIQUE,
|
|
6523
|
+
type TEXT NOT NULL,
|
|
6524
|
+
object_id TEXT NOT NULL DEFAULT '',
|
|
6525
|
+
object_type TEXT NOT NULL DEFAULT '',
|
|
6526
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
6527
|
+
processed_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
6528
|
+
status TEXT NOT NULL DEFAULT 'processed',
|
|
6529
|
+
error TEXT
|
|
6530
|
+
)
|
|
6531
|
+
`).run();
|
|
6532
|
+
await this.db.prepare(`
|
|
6533
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_type ON stripe_events(type)
|
|
6534
|
+
`).run();
|
|
6535
|
+
await this.db.prepare(`
|
|
6536
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_status ON stripe_events(status)
|
|
6537
|
+
`).run();
|
|
6538
|
+
await this.db.prepare(`
|
|
6539
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_events_processed_at ON stripe_events(processed_at DESC)
|
|
6540
|
+
`).run();
|
|
6541
|
+
}
|
|
6542
|
+
async log(event) {
|
|
6543
|
+
await this.db.prepare(`
|
|
6544
|
+
INSERT INTO stripe_events (stripe_event_id, type, object_id, object_type, data, status, error)
|
|
6545
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
6546
|
+
ON CONFLICT(stripe_event_id) DO UPDATE SET
|
|
6547
|
+
status = excluded.status,
|
|
6548
|
+
error = excluded.error,
|
|
6549
|
+
processed_at = unixepoch()
|
|
6550
|
+
`).bind(
|
|
6551
|
+
event.stripeEventId,
|
|
6552
|
+
event.type,
|
|
6553
|
+
event.objectId,
|
|
6554
|
+
event.objectType,
|
|
6555
|
+
JSON.stringify(event.data),
|
|
6556
|
+
event.status,
|
|
6557
|
+
event.error || null
|
|
6558
|
+
).run();
|
|
6559
|
+
}
|
|
6560
|
+
async list(filters = {}) {
|
|
6561
|
+
const where = [];
|
|
6562
|
+
const values = [];
|
|
6563
|
+
if (filters.type) {
|
|
6564
|
+
where.push("type = ?");
|
|
6565
|
+
values.push(filters.type);
|
|
6566
|
+
}
|
|
6567
|
+
if (filters.status) {
|
|
6568
|
+
where.push("status = ?");
|
|
6569
|
+
values.push(filters.status);
|
|
6570
|
+
}
|
|
6571
|
+
if (filters.objectId) {
|
|
6572
|
+
where.push("object_id = ?");
|
|
6573
|
+
values.push(filters.objectId);
|
|
6574
|
+
}
|
|
6575
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
6576
|
+
const limit = Math.min(filters.limit || 50, 100);
|
|
6577
|
+
const page = filters.page || 1;
|
|
6578
|
+
const offset = (page - 1) * limit;
|
|
6579
|
+
const countResult = await this.db.prepare(
|
|
6580
|
+
`SELECT COUNT(*) as count FROM stripe_events ${whereClause}`
|
|
6581
|
+
).bind(...values).first();
|
|
6582
|
+
const results = await this.db.prepare(
|
|
6583
|
+
`SELECT * FROM stripe_events ${whereClause} ORDER BY processed_at DESC LIMIT ? OFFSET ?`
|
|
6584
|
+
).bind(...values, limit, offset).all();
|
|
6585
|
+
return {
|
|
6586
|
+
events: (results.results || []).map((r) => this.mapRow(r)),
|
|
6587
|
+
total: countResult?.count || 0
|
|
6588
|
+
};
|
|
6589
|
+
}
|
|
6590
|
+
async getStats() {
|
|
6591
|
+
const result = await this.db.prepare(`
|
|
6592
|
+
SELECT
|
|
6593
|
+
COUNT(*) as total,
|
|
6594
|
+
SUM(CASE WHEN status = 'processed' THEN 1 ELSE 0 END) as processed,
|
|
6595
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
|
|
6596
|
+
SUM(CASE WHEN status = 'ignored' THEN 1 ELSE 0 END) as ignored
|
|
6597
|
+
FROM stripe_events
|
|
6598
|
+
`).first();
|
|
6599
|
+
return {
|
|
6600
|
+
total: result?.total || 0,
|
|
6601
|
+
processed: result?.processed || 0,
|
|
6602
|
+
failed: result?.failed || 0,
|
|
6603
|
+
ignored: result?.ignored || 0
|
|
6604
|
+
};
|
|
6605
|
+
}
|
|
6606
|
+
async getDistinctTypes() {
|
|
6607
|
+
const results = await this.db.prepare(
|
|
6608
|
+
"SELECT DISTINCT type FROM stripe_events ORDER BY type"
|
|
6609
|
+
).all();
|
|
6610
|
+
return (results.results || []).map((r) => r.type);
|
|
6611
|
+
}
|
|
6612
|
+
mapRow(row) {
|
|
6613
|
+
return {
|
|
6614
|
+
id: row.id,
|
|
6615
|
+
stripeEventId: row.stripe_event_id,
|
|
6616
|
+
type: row.type,
|
|
6617
|
+
objectId: row.object_id,
|
|
6618
|
+
objectType: row.object_type,
|
|
6619
|
+
data: row.data,
|
|
6620
|
+
processedAt: row.processed_at,
|
|
6621
|
+
status: row.status,
|
|
6622
|
+
error: row.error || void 0
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
};
|
|
6626
|
+
|
|
6627
|
+
// src/plugins/core-plugins/stripe-plugin/components/subscriptions-page.ts
|
|
6628
|
+
init_admin_layout_catalyst_template();
|
|
6629
|
+
|
|
6630
|
+
// src/plugins/core-plugins/stripe-plugin/components/tab-bar.ts
|
|
6631
|
+
var TABS = [
|
|
6632
|
+
{ label: "Subscriptions", path: "/admin/plugins/stripe" },
|
|
6633
|
+
{ label: "Events", path: "/admin/plugins/stripe/events" },
|
|
6634
|
+
{ label: "Settings", path: "/admin/plugins/stripe/settings" }
|
|
6635
|
+
];
|
|
6636
|
+
function renderStripeTabBar(currentPath) {
|
|
6637
|
+
const tabs = TABS.map((tab) => {
|
|
6638
|
+
const isActive = currentPath === tab.path || tab.path === "/admin/plugins/stripe" && currentPath === "/admin/plugins/stripe/";
|
|
6639
|
+
return `
|
|
6640
|
+
<a href="${tab.path}"
|
|
6641
|
+
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">
|
|
6642
|
+
${tab.label}
|
|
6643
|
+
</a>`;
|
|
6644
|
+
}).join("");
|
|
6645
|
+
return `
|
|
6646
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 mb-6">
|
|
6647
|
+
<nav class="-mb-px flex gap-x-2" aria-label="Stripe tabs">
|
|
6648
|
+
${tabs}
|
|
6649
|
+
</nav>
|
|
6650
|
+
</div>
|
|
6651
|
+
`;
|
|
6652
|
+
}
|
|
6653
|
+
|
|
6654
|
+
// src/plugins/core-plugins/stripe-plugin/components/subscriptions-page.ts
|
|
6655
|
+
function renderSubscriptionsPage(data) {
|
|
6656
|
+
const { subscriptions, stats, filters, user, version, dynamicMenuItems } = data;
|
|
6657
|
+
const content2 = `
|
|
6658
|
+
<div>
|
|
6659
|
+
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
|
6660
|
+
<div class="sm:flex-auto">
|
|
6661
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
6662
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
6663
|
+
Manage subscriptions, view billing status, and monitor payment events.
|
|
6664
|
+
</p>
|
|
6665
|
+
</div>
|
|
6666
|
+
<div class="mt-4 sm:mt-0 sm:ml-16">
|
|
6667
|
+
<button id="sync-btn" onclick="syncSubscriptions()"
|
|
6668
|
+
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">
|
|
6669
|
+
Sync from Stripe
|
|
6670
|
+
</button>
|
|
6671
|
+
</div>
|
|
6672
|
+
</div>
|
|
6673
|
+
|
|
6674
|
+
${renderStripeTabBar("/admin/plugins/stripe")}
|
|
6675
|
+
|
|
6676
|
+
<!-- Stats Cards -->
|
|
6677
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5 mb-6">
|
|
6678
|
+
${statsCard("Total", stats.total, "text-zinc-950 dark:text-white")}
|
|
6679
|
+
${statsCard("Active", stats.active, "text-emerald-600 dark:text-emerald-400")}
|
|
6680
|
+
${statsCard("Trialing", stats.trialing, "text-blue-600 dark:text-blue-400")}
|
|
6681
|
+
${statsCard("Past Due", stats.pastDue, "text-amber-600 dark:text-amber-400")}
|
|
6682
|
+
${statsCard("Canceled", stats.canceled, "text-red-600 dark:text-red-400")}
|
|
6683
|
+
</div>
|
|
6684
|
+
|
|
6685
|
+
<!-- Filters -->
|
|
6686
|
+
<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">
|
|
6687
|
+
<form method="GET" class="flex items-center gap-4">
|
|
6688
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Status:</label>
|
|
6689
|
+
<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()">
|
|
6690
|
+
<option value="">All</option>
|
|
6691
|
+
${statusOption("active", filters.status)}
|
|
6692
|
+
${statusOption("trialing", filters.status)}
|
|
6693
|
+
${statusOption("past_due", filters.status)}
|
|
6694
|
+
${statusOption("canceled", filters.status)}
|
|
6695
|
+
${statusOption("unpaid", filters.status)}
|
|
6696
|
+
${statusOption("paused", filters.status)}
|
|
6697
|
+
</select>
|
|
6698
|
+
</form>
|
|
6699
|
+
</div>
|
|
6700
|
+
|
|
6701
|
+
<!-- Subscriptions Table -->
|
|
6702
|
+
<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">
|
|
6703
|
+
<table class="min-w-full divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6704
|
+
<thead>
|
|
6705
|
+
<tr>
|
|
6706
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">User</th>
|
|
6707
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
|
6708
|
+
<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>
|
|
6709
|
+
<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>
|
|
6710
|
+
<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>
|
|
6711
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Stripe</th>
|
|
6712
|
+
</tr>
|
|
6713
|
+
</thead>
|
|
6714
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6715
|
+
${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("")}
|
|
6716
|
+
</tbody>
|
|
6717
|
+
</table>
|
|
6718
|
+
|
|
6719
|
+
${renderPagination2(filters.page, filters.totalPages, filters.status)}
|
|
6720
|
+
</div>
|
|
6721
|
+
|
|
6722
|
+
<div id="sync-message" class="hidden mt-4 rounded-lg p-4 text-sm"></div>
|
|
6723
|
+
</div>
|
|
6724
|
+
|
|
6725
|
+
<script>
|
|
6726
|
+
async function syncSubscriptions() {
|
|
6727
|
+
const btn = document.getElementById('sync-btn')
|
|
6728
|
+
const msg = document.getElementById('sync-message')
|
|
6729
|
+
btn.disabled = true
|
|
6730
|
+
btn.textContent = 'Syncing...'
|
|
6731
|
+
msg.className = 'hidden mt-4 rounded-lg p-4 text-sm'
|
|
6732
|
+
try {
|
|
6733
|
+
const res = await fetch('/api/stripe/sync-subscriptions', { method: 'POST' })
|
|
6734
|
+
const result = await res.json()
|
|
6735
|
+
if (result.success) {
|
|
6736
|
+
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'
|
|
6737
|
+
msg.textContent = 'Synced ' + result.synced + ' of ' + result.total + ' subscriptions from Stripe.' + (result.errors > 0 ? ' (' + result.errors + ' errors)' : '')
|
|
6738
|
+
setTimeout(() => location.reload(), 1500)
|
|
6739
|
+
} else {
|
|
6740
|
+
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'
|
|
6741
|
+
msg.textContent = result.error || 'Sync failed.'
|
|
6742
|
+
}
|
|
6743
|
+
} catch {
|
|
6744
|
+
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'
|
|
6745
|
+
msg.textContent = 'Network error. Please try again.'
|
|
6746
|
+
}
|
|
6747
|
+
btn.disabled = false
|
|
6748
|
+
btn.textContent = 'Sync from Stripe'
|
|
6749
|
+
}
|
|
6750
|
+
</script>
|
|
6751
|
+
`;
|
|
6752
|
+
const layoutData = {
|
|
6753
|
+
title: "Stripe Subscriptions",
|
|
6754
|
+
pageTitle: "Stripe Subscriptions",
|
|
6755
|
+
currentPath: "/admin/plugins/stripe",
|
|
6756
|
+
user,
|
|
6757
|
+
content: content2,
|
|
6758
|
+
version,
|
|
6759
|
+
dynamicMenuItems
|
|
6760
|
+
};
|
|
6761
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
6762
|
+
}
|
|
6763
|
+
function statsCard(label, value, colorClass) {
|
|
6764
|
+
return `
|
|
6765
|
+
<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">
|
|
6766
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${label}</p>
|
|
6767
|
+
<p class="mt-2 text-3xl font-bold ${colorClass}">${value}</p>
|
|
6768
|
+
</div>
|
|
6769
|
+
`;
|
|
6770
|
+
}
|
|
6771
|
+
function statusOption(value, current) {
|
|
6772
|
+
const selected = value === current ? "selected" : "";
|
|
6773
|
+
const label = value.replace("_", " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
6774
|
+
return `<option value="${value}" ${selected}>${label}</option>`;
|
|
6775
|
+
}
|
|
6776
|
+
function statusBadge(status) {
|
|
6777
|
+
const colors = {
|
|
6778
|
+
active: "bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-emerald-400/20",
|
|
6779
|
+
trialing: "bg-blue-400/10 text-blue-500 dark:text-blue-400 ring-blue-400/20",
|
|
6780
|
+
past_due: "bg-amber-400/10 text-amber-500 dark:text-amber-400 ring-amber-400/20",
|
|
6781
|
+
canceled: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20",
|
|
6782
|
+
unpaid: "bg-orange-400/10 text-orange-500 dark:text-orange-400 ring-orange-400/20",
|
|
6783
|
+
paused: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20",
|
|
6784
|
+
incomplete: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20",
|
|
6785
|
+
incomplete_expired: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20"
|
|
6786
|
+
};
|
|
6787
|
+
const color = colors[status] || "bg-zinc-400/10 text-zinc-500 ring-zinc-400/20";
|
|
6788
|
+
const label = status.replace("_", " ");
|
|
6789
|
+
return `<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset ${color}">${label}</span>`;
|
|
6790
|
+
}
|
|
6791
|
+
function formatDate(timestamp) {
|
|
6792
|
+
if (!timestamp) return "-";
|
|
6793
|
+
return new Date(timestamp * 1e3).toLocaleDateString("en-US", {
|
|
6794
|
+
month: "short",
|
|
6795
|
+
day: "numeric",
|
|
6796
|
+
year: "numeric"
|
|
6797
|
+
});
|
|
6798
|
+
}
|
|
6799
|
+
function renderRow(sub) {
|
|
6800
|
+
return `
|
|
6801
|
+
<tr class="hover:bg-zinc-950/[0.025] dark:hover:bg-white/[0.025]">
|
|
6802
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6803
|
+
<div class="text-sm font-medium text-zinc-950 dark:text-white">${sub.userEmail || sub.userId}</div>
|
|
6804
|
+
<div class="text-xs text-zinc-500 dark:text-zinc-400">${sub.stripeCustomerId}</div>
|
|
6805
|
+
</td>
|
|
6806
|
+
<td class="px-6 py-4 whitespace-nowrap">${statusBadge(sub.status)}</td>
|
|
6807
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">${sub.stripePriceId}</td>
|
|
6808
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
|
6809
|
+
${formatDate(sub.currentPeriodStart)} - ${formatDate(sub.currentPeriodEnd)}
|
|
6810
|
+
</td>
|
|
6811
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6812
|
+
${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>'}
|
|
6813
|
+
</td>
|
|
6814
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6815
|
+
<a href="https://dashboard.stripe.com/subscriptions/${sub.stripeSubscriptionId}"
|
|
6816
|
+
target="_blank" rel="noopener noreferrer"
|
|
6817
|
+
class="text-cyan-600 dark:text-cyan-400 hover:text-cyan-500 dark:hover:text-cyan-300">
|
|
6818
|
+
View in Stripe
|
|
6819
|
+
</a>
|
|
6820
|
+
</td>
|
|
6821
|
+
</tr>
|
|
6822
|
+
`;
|
|
6823
|
+
}
|
|
6824
|
+
function renderPagination2(page, totalPages, status) {
|
|
6825
|
+
if (totalPages <= 1) return "";
|
|
6826
|
+
const params = status ? `&status=${status}` : "";
|
|
6827
|
+
return `
|
|
6828
|
+
<div class="px-6 py-3 flex items-center justify-between border-t border-zinc-950/5 dark:border-white/5">
|
|
6829
|
+
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
6830
|
+
Page ${page} of ${totalPages}
|
|
6831
|
+
</div>
|
|
6832
|
+
<div class="flex gap-2">
|
|
6833
|
+
${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>` : ""}
|
|
6834
|
+
${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>` : ""}
|
|
6835
|
+
</div>
|
|
6836
|
+
</div>
|
|
6837
|
+
`;
|
|
6838
|
+
}
|
|
6839
|
+
|
|
6840
|
+
// src/plugins/core-plugins/stripe-plugin/components/events-page.ts
|
|
6841
|
+
init_admin_layout_catalyst_template();
|
|
6842
|
+
function renderEventsPage(data) {
|
|
6843
|
+
const { events, stats, types, filters, user, version, dynamicMenuItems } = data;
|
|
6844
|
+
const content2 = `
|
|
6845
|
+
<div>
|
|
6846
|
+
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
|
6847
|
+
<div class="sm:flex-auto">
|
|
6848
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
6849
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
6850
|
+
Webhook event log showing all processed, failed, and ignored Stripe events.
|
|
6851
|
+
</p>
|
|
6852
|
+
</div>
|
|
6853
|
+
</div>
|
|
6854
|
+
|
|
6855
|
+
${renderStripeTabBar("/admin/plugins/stripe/events")}
|
|
6856
|
+
|
|
6857
|
+
<!-- Stats Cards -->
|
|
6858
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
|
6859
|
+
${eventStatsCard("Total Events", stats.total, "text-zinc-950 dark:text-white")}
|
|
6860
|
+
${eventStatsCard("Processed", stats.processed, "text-emerald-600 dark:text-emerald-400")}
|
|
6861
|
+
${eventStatsCard("Failed", stats.failed, "text-red-600 dark:text-red-400")}
|
|
6862
|
+
${eventStatsCard("Ignored", stats.ignored, "text-zinc-500 dark:text-zinc-400")}
|
|
6863
|
+
</div>
|
|
6864
|
+
|
|
6865
|
+
<!-- Filters -->
|
|
6866
|
+
<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">
|
|
6867
|
+
<form method="GET" class="flex items-center gap-4 flex-wrap">
|
|
6868
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Type:</label>
|
|
6869
|
+
<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()">
|
|
6870
|
+
<option value="">All</option>
|
|
6871
|
+
${types.map((t) => `<option value="${t}" ${t === filters.type ? "selected" : ""}>${t}</option>`).join("")}
|
|
6872
|
+
</select>
|
|
6873
|
+
|
|
6874
|
+
<label class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Status:</label>
|
|
6875
|
+
<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()">
|
|
6876
|
+
<option value="">All</option>
|
|
6877
|
+
${eventStatusOption("processed", filters.status)}
|
|
6878
|
+
${eventStatusOption("failed", filters.status)}
|
|
6879
|
+
${eventStatusOption("ignored", filters.status)}
|
|
6880
|
+
</select>
|
|
6881
|
+
</form>
|
|
6882
|
+
</div>
|
|
6883
|
+
|
|
6884
|
+
<!-- Events Table -->
|
|
6885
|
+
<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">
|
|
6886
|
+
<table class="min-w-full divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6887
|
+
<thead>
|
|
6888
|
+
<tr>
|
|
6889
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Time</th>
|
|
6890
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Type</th>
|
|
6891
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Object</th>
|
|
6892
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
|
6893
|
+
<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>
|
|
6894
|
+
</tr>
|
|
6895
|
+
</thead>
|
|
6896
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
|
|
6897
|
+
${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("")}
|
|
6898
|
+
</tbody>
|
|
6899
|
+
</table>
|
|
6900
|
+
|
|
6901
|
+
${renderEventPagination(filters.page, filters.totalPages, filters.type, filters.status)}
|
|
6902
|
+
</div>
|
|
6903
|
+
</div>
|
|
6904
|
+
`;
|
|
6905
|
+
const layoutData = {
|
|
6906
|
+
title: "Stripe Events",
|
|
6907
|
+
pageTitle: "Stripe Events",
|
|
6908
|
+
currentPath: "/admin/plugins/stripe",
|
|
6909
|
+
user,
|
|
6910
|
+
content: content2,
|
|
6911
|
+
version,
|
|
6912
|
+
dynamicMenuItems
|
|
6913
|
+
};
|
|
6914
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
6915
|
+
}
|
|
6916
|
+
function eventStatsCard(label, value, colorClass) {
|
|
6917
|
+
return `
|
|
6918
|
+
<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">
|
|
6919
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${label}</p>
|
|
6920
|
+
<p class="mt-2 text-3xl font-bold ${colorClass}">${value}</p>
|
|
6921
|
+
</div>
|
|
6922
|
+
`;
|
|
6923
|
+
}
|
|
6924
|
+
function eventStatusOption(value, current) {
|
|
6925
|
+
const selected = value === current ? "selected" : "";
|
|
6926
|
+
const label = value.charAt(0).toUpperCase() + value.slice(1);
|
|
6927
|
+
return `<option value="${value}" ${selected}>${label}</option>`;
|
|
6928
|
+
}
|
|
6929
|
+
function eventStatusBadge(status) {
|
|
6930
|
+
const colors = {
|
|
6931
|
+
processed: "bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 ring-emerald-400/20",
|
|
6932
|
+
failed: "bg-red-400/10 text-red-500 dark:text-red-400 ring-red-400/20",
|
|
6933
|
+
ignored: "bg-zinc-400/10 text-zinc-500 dark:text-zinc-400 ring-zinc-400/20"
|
|
6934
|
+
};
|
|
6935
|
+
const color = colors[status] || "bg-zinc-400/10 text-zinc-500 ring-zinc-400/20";
|
|
6936
|
+
return `<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset ${color}">${status}</span>`;
|
|
6937
|
+
}
|
|
6938
|
+
function formatTimestamp3(timestamp) {
|
|
6939
|
+
if (!timestamp) return "-";
|
|
6940
|
+
const d = new Date(timestamp * 1e3);
|
|
6941
|
+
return d.toLocaleString("en-US", {
|
|
6942
|
+
month: "short",
|
|
6943
|
+
day: "numeric",
|
|
6944
|
+
year: "numeric",
|
|
6945
|
+
hour: "2-digit",
|
|
6946
|
+
minute: "2-digit",
|
|
6947
|
+
second: "2-digit"
|
|
6948
|
+
});
|
|
6949
|
+
}
|
|
6950
|
+
function renderEventRow(event) {
|
|
6951
|
+
const errorTooltip = event.error ? ` title="${event.error.replace(/"/g, """)}"` : "";
|
|
6952
|
+
return `
|
|
6953
|
+
<tr class="hover:bg-zinc-950/[0.025] dark:hover:bg-white/[0.025]"${errorTooltip}>
|
|
6954
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
|
6955
|
+
${formatTimestamp3(event.processedAt)}
|
|
6956
|
+
</td>
|
|
6957
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6958
|
+
<span class="text-sm font-mono text-zinc-950 dark:text-white">${event.type}</span>
|
|
6959
|
+
</td>
|
|
6960
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6961
|
+
<div class="text-sm font-mono text-zinc-500 dark:text-zinc-400">${event.objectId || "-"}</div>
|
|
6962
|
+
<div class="text-xs text-zinc-400 dark:text-zinc-500">${event.objectType}</div>
|
|
6963
|
+
</td>
|
|
6964
|
+
<td class="px-6 py-4 whitespace-nowrap">${eventStatusBadge(event.status)}</td>
|
|
6965
|
+
<td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-zinc-400 dark:text-zinc-500">${event.stripeEventId}</td>
|
|
6966
|
+
</tr>
|
|
6967
|
+
`;
|
|
6968
|
+
}
|
|
6969
|
+
function renderEventPagination(page, totalPages, type, status) {
|
|
6970
|
+
if (totalPages <= 1) return "";
|
|
6971
|
+
const params = [];
|
|
6972
|
+
if (type) params.push(`type=${type}`);
|
|
6973
|
+
if (status) params.push(`status=${status}`);
|
|
6974
|
+
const extra = params.length > 0 ? `&${params.join("&")}` : "";
|
|
6975
|
+
return `
|
|
6976
|
+
<div class="px-6 py-3 flex items-center justify-between border-t border-zinc-950/5 dark:border-white/5">
|
|
6977
|
+
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
6978
|
+
Page ${page} of ${totalPages}
|
|
6979
|
+
</div>
|
|
6980
|
+
<div class="flex gap-2">
|
|
6981
|
+
${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>` : ""}
|
|
6982
|
+
${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>` : ""}
|
|
6983
|
+
</div>
|
|
6984
|
+
</div>
|
|
6985
|
+
`;
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6988
|
+
// src/plugins/core-plugins/stripe-plugin/types.ts
|
|
6989
|
+
var DEFAULT_SETTINGS3 = {
|
|
6990
|
+
stripePublishableKey: "",
|
|
6991
|
+
stripeSecretKey: "",
|
|
6992
|
+
stripeWebhookSecret: "",
|
|
6993
|
+
stripePriceId: "",
|
|
6994
|
+
successUrl: "/admin/dashboard",
|
|
6995
|
+
cancelUrl: "/admin/dashboard"
|
|
6996
|
+
};
|
|
6997
|
+
|
|
6998
|
+
// src/plugins/core-plugins/stripe-plugin/routes/admin.ts
|
|
6999
|
+
var adminRoutes3 = new Hono();
|
|
7000
|
+
adminRoutes3.use("*", requireAuth());
|
|
7001
|
+
adminRoutes3.use("*", async (c, next) => {
|
|
7002
|
+
const user = c.get("user");
|
|
7003
|
+
if (user?.role !== "admin") {
|
|
7004
|
+
return c.text("Access denied", 403);
|
|
7005
|
+
}
|
|
7006
|
+
return next();
|
|
7007
|
+
});
|
|
7008
|
+
async function getSettings3(db) {
|
|
7009
|
+
try {
|
|
7010
|
+
const pluginService = new PluginService(db);
|
|
7011
|
+
const plugin2 = await pluginService.getPlugin("stripe");
|
|
7012
|
+
if (plugin2?.settings) {
|
|
7013
|
+
const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
|
|
7014
|
+
return { ...DEFAULT_SETTINGS3, ...settings };
|
|
7015
|
+
}
|
|
7016
|
+
} catch {
|
|
7017
|
+
}
|
|
7018
|
+
return DEFAULT_SETTINGS3;
|
|
7019
|
+
}
|
|
7020
|
+
adminRoutes3.get("/", async (c) => {
|
|
7021
|
+
const db = c.env.DB;
|
|
7022
|
+
const user = c.get("user");
|
|
7023
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7024
|
+
await subscriptionService.ensureTable();
|
|
7025
|
+
const page = parseInt(c.req.query("page") || "1");
|
|
7026
|
+
const limit = 50;
|
|
7027
|
+
const statusFilter = c.req.query("status");
|
|
7028
|
+
const [{ subscriptions, total }, stats] = await Promise.all([
|
|
7029
|
+
subscriptionService.list({ status: statusFilter, page, limit }),
|
|
7030
|
+
subscriptionService.getStats()
|
|
7031
|
+
]);
|
|
7032
|
+
const totalPages = Math.ceil(total / limit);
|
|
7033
|
+
const html = renderSubscriptionsPage({
|
|
7034
|
+
subscriptions,
|
|
7035
|
+
stats,
|
|
7036
|
+
filters: { status: statusFilter, page, totalPages },
|
|
7037
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7038
|
+
version: c.get("appVersion"),
|
|
7039
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7040
|
+
});
|
|
7041
|
+
return c.html(html);
|
|
7042
|
+
});
|
|
7043
|
+
adminRoutes3.get("/events", async (c) => {
|
|
7044
|
+
const db = c.env.DB;
|
|
7045
|
+
const user = c.get("user");
|
|
7046
|
+
const eventService = new StripeEventService(db);
|
|
7047
|
+
await eventService.ensureTable();
|
|
7048
|
+
const page = parseInt(c.req.query("page") || "1");
|
|
7049
|
+
const limit = 50;
|
|
7050
|
+
const typeFilter = c.req.query("type") || void 0;
|
|
7051
|
+
const statusFilter = c.req.query("status");
|
|
7052
|
+
const [{ events, total }, stats, types] = await Promise.all([
|
|
7053
|
+
eventService.list({ type: typeFilter, status: statusFilter, page, limit }),
|
|
7054
|
+
eventService.getStats(),
|
|
7055
|
+
eventService.getDistinctTypes()
|
|
7056
|
+
]);
|
|
7057
|
+
const totalPages = Math.ceil(total / limit);
|
|
7058
|
+
const html = renderEventsPage({
|
|
7059
|
+
events,
|
|
7060
|
+
stats,
|
|
7061
|
+
types,
|
|
7062
|
+
filters: { type: typeFilter, status: statusFilter, page, totalPages },
|
|
7063
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7064
|
+
version: c.get("appVersion"),
|
|
7065
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7066
|
+
});
|
|
7067
|
+
return c.html(html);
|
|
7068
|
+
});
|
|
7069
|
+
adminRoutes3.get("/settings", async (c) => {
|
|
7070
|
+
const db = c.env.DB;
|
|
7071
|
+
const user = c.get("user");
|
|
7072
|
+
const settings = await getSettings3(db);
|
|
7073
|
+
const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-UMTIN66R.js');
|
|
7074
|
+
const content2 = `
|
|
7075
|
+
<div>
|
|
7076
|
+
<div class="mb-6">
|
|
7077
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Stripe</h1>
|
|
7078
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
7079
|
+
Configure your Stripe API keys and checkout options.
|
|
7080
|
+
</p>
|
|
7081
|
+
</div>
|
|
7082
|
+
|
|
7083
|
+
${renderStripeTabBar("/admin/plugins/stripe/settings")}
|
|
7084
|
+
|
|
7085
|
+
<div id="settings-message" class="hidden mb-4 rounded-lg p-4 text-sm"></div>
|
|
7086
|
+
|
|
7087
|
+
<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">
|
|
7088
|
+
<div class="p-6 space-y-5">
|
|
7089
|
+
<div>
|
|
7090
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Publishable Key</label>
|
|
7091
|
+
<input type="text" name="stripePublishableKey" value="${settings.stripePublishableKey}"
|
|
7092
|
+
placeholder="pk_..."
|
|
7093
|
+
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" />
|
|
7094
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Your Stripe publishable key (starts with pk_)</p>
|
|
7095
|
+
</div>
|
|
7096
|
+
|
|
7097
|
+
<div>
|
|
7098
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Secret Key</label>
|
|
7099
|
+
<input type="password" name="stripeSecretKey" value="${settings.stripeSecretKey}"
|
|
7100
|
+
placeholder="sk_..."
|
|
7101
|
+
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" />
|
|
7102
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Your Stripe secret API key (starts with sk_)</p>
|
|
7103
|
+
</div>
|
|
7104
|
+
|
|
7105
|
+
<div>
|
|
7106
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Webhook Signing Secret</label>
|
|
7107
|
+
<input type="password" name="stripeWebhookSecret" value="${settings.stripeWebhookSecret}"
|
|
7108
|
+
placeholder="whsec_..."
|
|
7109
|
+
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" />
|
|
7110
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Stripe webhook endpoint signing secret (starts with whsec_)</p>
|
|
7111
|
+
</div>
|
|
7112
|
+
|
|
7113
|
+
<div>
|
|
7114
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Default Price ID</label>
|
|
7115
|
+
<input type="text" name="stripePriceId" value="${settings.stripePriceId || ""}"
|
|
7116
|
+
placeholder="price_..."
|
|
7117
|
+
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" />
|
|
7118
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Default Stripe Price ID for checkout sessions (optional)</p>
|
|
7119
|
+
</div>
|
|
7120
|
+
|
|
7121
|
+
<div>
|
|
7122
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Checkout Success URL</label>
|
|
7123
|
+
<input type="text" name="successUrl" value="${settings.successUrl}"
|
|
7124
|
+
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" />
|
|
7125
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Redirect URL after successful checkout</p>
|
|
7126
|
+
</div>
|
|
7127
|
+
|
|
7128
|
+
<div>
|
|
7129
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-1.5">Checkout Cancel URL</label>
|
|
7130
|
+
<input type="text" name="cancelUrl" value="${settings.cancelUrl}"
|
|
7131
|
+
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" />
|
|
7132
|
+
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Redirect URL if checkout is cancelled</p>
|
|
7133
|
+
</div>
|
|
7134
|
+
</div>
|
|
7135
|
+
|
|
7136
|
+
<div class="px-6 py-4 flex justify-end">
|
|
7137
|
+
<button type="submit"
|
|
7138
|
+
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">
|
|
7139
|
+
Save Settings
|
|
7140
|
+
</button>
|
|
7141
|
+
</div>
|
|
7142
|
+
</form>
|
|
7143
|
+
</div>
|
|
7144
|
+
|
|
7145
|
+
<script>
|
|
7146
|
+
document.getElementById('stripe-settings-form').addEventListener('submit', async (e) => {
|
|
7147
|
+
e.preventDefault()
|
|
7148
|
+
const form = e.target
|
|
7149
|
+
const data = Object.fromEntries(new FormData(form))
|
|
7150
|
+
const msg = document.getElementById('settings-message')
|
|
7151
|
+
try {
|
|
7152
|
+
const res = await fetch('/admin/plugins/stripe/settings', {
|
|
7153
|
+
method: 'POST',
|
|
7154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7155
|
+
body: JSON.stringify(data)
|
|
7156
|
+
})
|
|
7157
|
+
const result = await res.json()
|
|
7158
|
+
msg.className = result.success
|
|
7159
|
+
? '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'
|
|
7160
|
+
: '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'
|
|
7161
|
+
msg.textContent = result.success ? 'Settings saved successfully.' : (result.error || 'Failed to save settings.')
|
|
7162
|
+
} catch {
|
|
7163
|
+
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'
|
|
7164
|
+
msg.textContent = 'Network error. Please try again.'
|
|
7165
|
+
}
|
|
7166
|
+
})
|
|
7167
|
+
</script>
|
|
7168
|
+
`;
|
|
7169
|
+
return c.html(renderAdminLayoutCatalyst2({
|
|
7170
|
+
title: "Stripe Settings",
|
|
7171
|
+
pageTitle: "Stripe Settings",
|
|
7172
|
+
currentPath: "/admin/plugins/stripe",
|
|
7173
|
+
user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
|
|
7174
|
+
content: content2,
|
|
7175
|
+
version: c.get("appVersion"),
|
|
7176
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7177
|
+
}));
|
|
7178
|
+
});
|
|
7179
|
+
adminRoutes3.post("/settings", async (c) => {
|
|
7180
|
+
try {
|
|
7181
|
+
const body = await c.req.json();
|
|
7182
|
+
const db = c.env.DB;
|
|
7183
|
+
await db.prepare(`
|
|
7184
|
+
UPDATE plugins
|
|
7185
|
+
SET settings = ?,
|
|
7186
|
+
updated_at = unixepoch()
|
|
7187
|
+
WHERE id = 'stripe'
|
|
7188
|
+
`).bind(JSON.stringify(body)).run();
|
|
7189
|
+
return c.json({ success: true });
|
|
7190
|
+
} catch (error) {
|
|
7191
|
+
console.error("Error saving Stripe settings:", error);
|
|
7192
|
+
return c.json({ success: false, error: "Failed to save settings" }, 500);
|
|
7193
|
+
}
|
|
7194
|
+
});
|
|
7195
|
+
|
|
7196
|
+
// src/plugins/core-plugins/stripe-plugin/services/stripe-api.ts
|
|
7197
|
+
var StripeAPI = class {
|
|
7198
|
+
constructor(secretKey) {
|
|
7199
|
+
this.secretKey = secretKey;
|
|
7200
|
+
}
|
|
7201
|
+
baseUrl = "https://api.stripe.com/v1";
|
|
7202
|
+
/**
|
|
7203
|
+
* Verify a webhook signature
|
|
7204
|
+
* Implements Stripe's v1 signature scheme using Web Crypto API
|
|
7205
|
+
*/
|
|
7206
|
+
async verifyWebhookSignature(payload, sigHeader, secret) {
|
|
7207
|
+
const parts = sigHeader.split(",");
|
|
7208
|
+
const timestamp = parts.find((p) => p.startsWith("t="))?.split("=")[1];
|
|
7209
|
+
const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.substring(3));
|
|
7210
|
+
if (!timestamp || signatures.length === 0) return false;
|
|
7211
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7212
|
+
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
|
|
7213
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
7214
|
+
const encoder = new TextEncoder();
|
|
7215
|
+
const key = await crypto.subtle.importKey(
|
|
7216
|
+
"raw",
|
|
7217
|
+
encoder.encode(secret),
|
|
7218
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
7219
|
+
false,
|
|
7220
|
+
["sign"]
|
|
7221
|
+
);
|
|
7222
|
+
const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload));
|
|
7223
|
+
const expectedSignature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7224
|
+
return signatures.some((sig) => timingSafeEqual(sig, expectedSignature));
|
|
7225
|
+
}
|
|
7226
|
+
/**
|
|
7227
|
+
* Create a Checkout Session
|
|
7228
|
+
*/
|
|
7229
|
+
async createCheckoutSession(params) {
|
|
7230
|
+
const body = new URLSearchParams();
|
|
7231
|
+
body.append("mode", "subscription");
|
|
7232
|
+
body.append("line_items[0][price]", params.priceId);
|
|
7233
|
+
body.append("line_items[0][quantity]", "1");
|
|
7234
|
+
body.append("success_url", params.successUrl);
|
|
7235
|
+
body.append("cancel_url", params.cancelUrl);
|
|
7236
|
+
if (params.customerId) {
|
|
7237
|
+
body.append("customer", params.customerId);
|
|
7238
|
+
} else if (params.customerEmail) {
|
|
7239
|
+
body.append("customer_email", params.customerEmail);
|
|
7240
|
+
}
|
|
7241
|
+
if (params.metadata) {
|
|
7242
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
7243
|
+
body.append(`metadata[${key}]`, value);
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
const response = await this.request("POST", "/checkout/sessions", body);
|
|
7247
|
+
return { id: response.id, url: response.url };
|
|
7248
|
+
}
|
|
7249
|
+
/**
|
|
7250
|
+
* Retrieve a Stripe subscription
|
|
7251
|
+
*/
|
|
7252
|
+
async getSubscription(subscriptionId) {
|
|
7253
|
+
return this.request("GET", `/subscriptions/${subscriptionId}`);
|
|
7254
|
+
}
|
|
7255
|
+
/**
|
|
7256
|
+
* Create a Stripe customer
|
|
7257
|
+
*/
|
|
7258
|
+
async createCustomer(params) {
|
|
7259
|
+
const body = new URLSearchParams();
|
|
7260
|
+
body.append("email", params.email);
|
|
7261
|
+
if (params.metadata) {
|
|
7262
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
7263
|
+
body.append(`metadata[${key}]`, value);
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
return this.request("POST", "/customers", body);
|
|
7267
|
+
}
|
|
7268
|
+
/**
|
|
7269
|
+
* List subscriptions with pagination (auto-expands across pages)
|
|
7270
|
+
*/
|
|
7271
|
+
async listSubscriptions(params) {
|
|
7272
|
+
const qs = new URLSearchParams();
|
|
7273
|
+
qs.append("limit", String(params?.limit || 100));
|
|
7274
|
+
if (params?.status) qs.append("status", params.status);
|
|
7275
|
+
if (params?.startingAfter) qs.append("starting_after", params.startingAfter);
|
|
7276
|
+
return this.request("GET", `/subscriptions?${qs.toString()}`);
|
|
7277
|
+
}
|
|
7278
|
+
/**
|
|
7279
|
+
* Fetch ALL subscriptions from Stripe (handles pagination automatically)
|
|
7280
|
+
*/
|
|
7281
|
+
async listAllSubscriptions() {
|
|
7282
|
+
const all = [];
|
|
7283
|
+
let startingAfter;
|
|
7284
|
+
while (true) {
|
|
7285
|
+
const result = await this.listSubscriptions({ limit: 100, startingAfter });
|
|
7286
|
+
all.push(...result.data);
|
|
7287
|
+
if (!result.has_more || result.data.length === 0) break;
|
|
7288
|
+
startingAfter = result.data[result.data.length - 1].id;
|
|
7289
|
+
}
|
|
7290
|
+
return all;
|
|
7291
|
+
}
|
|
7292
|
+
/**
|
|
7293
|
+
* Search for a customer by email
|
|
7294
|
+
*/
|
|
7295
|
+
async findCustomerByEmail(email) {
|
|
7296
|
+
const params = new URLSearchParams();
|
|
7297
|
+
params.append("query", `email:'${email}'`);
|
|
7298
|
+
params.append("limit", "1");
|
|
7299
|
+
const result = await this.request("GET", `/customers/search?${params.toString()}`);
|
|
7300
|
+
return result.data?.[0] || null;
|
|
7301
|
+
}
|
|
7302
|
+
async request(method, path, body) {
|
|
7303
|
+
const url = path.startsWith("http") ? path : `${this.baseUrl}${path}`;
|
|
7304
|
+
const response = await fetch(url, {
|
|
7305
|
+
method,
|
|
7306
|
+
headers: {
|
|
7307
|
+
"Authorization": `Bearer ${this.secretKey}`,
|
|
7308
|
+
...body ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
|
|
7309
|
+
},
|
|
7310
|
+
...body ? { body: body.toString() } : {}
|
|
7311
|
+
});
|
|
7312
|
+
const data = await response.json();
|
|
7313
|
+
if (!response.ok) {
|
|
7314
|
+
throw new Error(`Stripe API error: ${data.error?.message || response.statusText}`);
|
|
7315
|
+
}
|
|
7316
|
+
return data;
|
|
7317
|
+
}
|
|
7318
|
+
};
|
|
7319
|
+
function timingSafeEqual(a, b) {
|
|
7320
|
+
if (a.length !== b.length) return false;
|
|
7321
|
+
let result = 0;
|
|
7322
|
+
for (let i = 0; i < a.length; i++) {
|
|
7323
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
7324
|
+
}
|
|
7325
|
+
return result === 0;
|
|
7326
|
+
}
|
|
7327
|
+
|
|
7328
|
+
// src/plugins/core-plugins/stripe-plugin/routes/api.ts
|
|
7329
|
+
var apiRoutes3 = new Hono();
|
|
7330
|
+
async function getSettings4(db) {
|
|
7331
|
+
try {
|
|
7332
|
+
const pluginService = new PluginService(db);
|
|
7333
|
+
const plugin2 = await pluginService.getPlugin("stripe");
|
|
7334
|
+
if (plugin2?.settings) {
|
|
7335
|
+
const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
|
|
7336
|
+
return { ...DEFAULT_SETTINGS3, ...settings };
|
|
7337
|
+
}
|
|
7338
|
+
} catch {
|
|
7339
|
+
}
|
|
7340
|
+
return DEFAULT_SETTINGS3;
|
|
7341
|
+
}
|
|
7342
|
+
function mapStripeStatus(status) {
|
|
7343
|
+
const map = {
|
|
7344
|
+
active: "active",
|
|
7345
|
+
canceled: "canceled",
|
|
7346
|
+
past_due: "past_due",
|
|
7347
|
+
trialing: "trialing",
|
|
7348
|
+
unpaid: "unpaid",
|
|
7349
|
+
paused: "paused",
|
|
7350
|
+
incomplete: "incomplete",
|
|
7351
|
+
incomplete_expired: "incomplete_expired"
|
|
7352
|
+
};
|
|
7353
|
+
return map[status] || "incomplete";
|
|
7354
|
+
}
|
|
7355
|
+
apiRoutes3.post("/webhook", async (c) => {
|
|
7356
|
+
const db = c.env.DB;
|
|
7357
|
+
const settings = await getSettings4(db);
|
|
7358
|
+
if (!settings.stripeWebhookSecret) {
|
|
7359
|
+
return c.json({ error: "Webhook secret not configured" }, 500);
|
|
7360
|
+
}
|
|
7361
|
+
const rawBody = await c.req.text();
|
|
7362
|
+
const sigHeader = c.req.header("stripe-signature") || "";
|
|
7363
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
7364
|
+
const isValid = await stripeApi.verifyWebhookSignature(rawBody, sigHeader, settings.stripeWebhookSecret);
|
|
7365
|
+
if (!isValid) {
|
|
7366
|
+
return c.json({ error: "Invalid signature" }, 400);
|
|
7367
|
+
}
|
|
7368
|
+
const event = JSON.parse(rawBody);
|
|
7369
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7370
|
+
const eventService = new StripeEventService(db);
|
|
7371
|
+
await Promise.all([subscriptionService.ensureTable(), eventService.ensureTable()]);
|
|
7372
|
+
const obj = event.data.object;
|
|
7373
|
+
const objectId = obj?.id || "";
|
|
7374
|
+
const objectType = obj?.object || event.type.split(".")[0] || "";
|
|
7375
|
+
try {
|
|
7376
|
+
switch (event.type) {
|
|
7377
|
+
case "customer.subscription.created": {
|
|
7378
|
+
const sub = event.data.object;
|
|
7379
|
+
const userId = sub.metadata?.sonicjs_user_id || await subscriptionService.getUserIdByStripeCustomer(sub.customer) || "";
|
|
7380
|
+
await subscriptionService.create({
|
|
7381
|
+
userId,
|
|
7382
|
+
stripeCustomerId: sub.customer,
|
|
7383
|
+
stripeSubscriptionId: sub.id,
|
|
7384
|
+
stripePriceId: sub.items.data[0]?.price.id || "",
|
|
7385
|
+
status: mapStripeStatus(sub.status),
|
|
7386
|
+
currentPeriodStart: sub.current_period_start,
|
|
7387
|
+
currentPeriodEnd: sub.current_period_end,
|
|
7388
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
7389
|
+
});
|
|
7390
|
+
console.log(`[Stripe] Subscription created: ${sub.id}`);
|
|
7391
|
+
break;
|
|
7392
|
+
}
|
|
7393
|
+
case "customer.subscription.updated": {
|
|
7394
|
+
const sub = event.data.object;
|
|
7395
|
+
await subscriptionService.updateByStripeId(sub.id, {
|
|
7396
|
+
status: mapStripeStatus(sub.status),
|
|
7397
|
+
stripePriceId: sub.items.data[0]?.price.id || void 0,
|
|
7398
|
+
currentPeriodStart: sub.current_period_start,
|
|
7399
|
+
currentPeriodEnd: sub.current_period_end,
|
|
7400
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
7401
|
+
});
|
|
7402
|
+
console.log(`[Stripe] Subscription updated: ${sub.id} -> ${sub.status}`);
|
|
7403
|
+
break;
|
|
7404
|
+
}
|
|
7405
|
+
case "customer.subscription.deleted": {
|
|
7406
|
+
const sub = event.data.object;
|
|
7407
|
+
await subscriptionService.updateByStripeId(sub.id, {
|
|
7408
|
+
status: "canceled"
|
|
7409
|
+
});
|
|
7410
|
+
console.log(`[Stripe] Subscription deleted: ${sub.id}`);
|
|
7411
|
+
break;
|
|
7412
|
+
}
|
|
7413
|
+
case "checkout.session.completed": {
|
|
7414
|
+
const session = event.data.object;
|
|
7415
|
+
const userId = session.metadata?.sonicjs_user_id;
|
|
7416
|
+
if (userId && session.subscription) {
|
|
7417
|
+
const existing = await subscriptionService.getByStripeSubscriptionId(session.subscription);
|
|
7418
|
+
if (existing && !existing.userId) {
|
|
7419
|
+
await subscriptionService.updateByStripeId(session.subscription, {
|
|
7420
|
+
userId
|
|
7421
|
+
});
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7424
|
+
console.log(`[Stripe] Checkout completed: ${session.id}`);
|
|
7425
|
+
break;
|
|
7426
|
+
}
|
|
7427
|
+
case "invoice.payment_succeeded": {
|
|
7428
|
+
const invoice = event.data.object;
|
|
7429
|
+
if (invoice.subscription) {
|
|
7430
|
+
await subscriptionService.updateByStripeId(invoice.subscription, {
|
|
7431
|
+
status: "active"
|
|
7432
|
+
});
|
|
7433
|
+
}
|
|
7434
|
+
console.log(`[Stripe] Payment succeeded for invoice: ${invoice.id}`);
|
|
7435
|
+
break;
|
|
7436
|
+
}
|
|
7437
|
+
case "invoice.payment_failed": {
|
|
7438
|
+
const invoice = event.data.object;
|
|
7439
|
+
if (invoice.subscription) {
|
|
7440
|
+
await subscriptionService.updateByStripeId(invoice.subscription, {
|
|
7441
|
+
status: "past_due"
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
7444
|
+
console.log(`[Stripe] Payment failed for invoice: ${invoice.id}`);
|
|
7445
|
+
break;
|
|
7446
|
+
}
|
|
7447
|
+
default:
|
|
7448
|
+
console.log(`[Stripe] Unhandled event type: ${event.type}`);
|
|
7449
|
+
await eventService.log({
|
|
7450
|
+
stripeEventId: event.id,
|
|
7451
|
+
type: event.type,
|
|
7452
|
+
objectId,
|
|
7453
|
+
objectType,
|
|
7454
|
+
data: event.data.object,
|
|
7455
|
+
status: "ignored"
|
|
7456
|
+
});
|
|
7457
|
+
return c.json({ received: true });
|
|
7458
|
+
}
|
|
7459
|
+
await eventService.log({
|
|
7460
|
+
stripeEventId: event.id,
|
|
7461
|
+
type: event.type,
|
|
7462
|
+
objectId,
|
|
7463
|
+
objectType,
|
|
7464
|
+
data: event.data.object,
|
|
7465
|
+
status: "processed"
|
|
7466
|
+
});
|
|
7467
|
+
} catch (error) {
|
|
7468
|
+
await eventService.log({
|
|
7469
|
+
stripeEventId: event.id,
|
|
7470
|
+
type: event.type,
|
|
7471
|
+
objectId,
|
|
7472
|
+
objectType,
|
|
7473
|
+
data: event.data.object,
|
|
7474
|
+
status: "failed",
|
|
7475
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7476
|
+
}).catch(() => {
|
|
7477
|
+
});
|
|
7478
|
+
console.error(`[Stripe] Error processing webhook event ${event.type}:`, error);
|
|
7479
|
+
return c.json({ error: "Webhook processing failed" }, 500);
|
|
7480
|
+
}
|
|
7481
|
+
return c.json({ received: true });
|
|
7482
|
+
});
|
|
7483
|
+
apiRoutes3.post("/create-checkout-session", requireAuth(), async (c) => {
|
|
7484
|
+
const db = c.env.DB;
|
|
7485
|
+
const user = c.get("user");
|
|
7486
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
7487
|
+
const settings = await getSettings4(db);
|
|
7488
|
+
if (!settings.stripeSecretKey) {
|
|
7489
|
+
return c.json({ error: "Stripe not configured" }, 500);
|
|
7490
|
+
}
|
|
7491
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7492
|
+
const priceId = body.priceId || settings.stripePriceId;
|
|
7493
|
+
if (!priceId) {
|
|
7494
|
+
return c.json({ error: "No price ID specified" }, 400);
|
|
7495
|
+
}
|
|
7496
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
7497
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7498
|
+
await subscriptionService.ensureTable();
|
|
7499
|
+
const existingSub = await subscriptionService.getByUserId(user.userId);
|
|
7500
|
+
let customerId = existingSub?.stripeCustomerId;
|
|
7501
|
+
if (!customerId) {
|
|
7502
|
+
const existing = await stripeApi.findCustomerByEmail(user.email);
|
|
7503
|
+
if (existing) {
|
|
7504
|
+
customerId = existing.id;
|
|
7505
|
+
} else {
|
|
7506
|
+
const customer = await stripeApi.createCustomer({
|
|
7507
|
+
email: user.email,
|
|
7508
|
+
metadata: { sonicjs_user_id: user.userId }
|
|
7509
|
+
});
|
|
7510
|
+
customerId = customer.id;
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
const origin = new URL(c.req.url).origin;
|
|
7514
|
+
const session = await stripeApi.createCheckoutSession({
|
|
7515
|
+
priceId,
|
|
7516
|
+
customerId,
|
|
7517
|
+
successUrl: `${origin}${settings.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
|
|
7518
|
+
cancelUrl: `${origin}${settings.cancelUrl}`,
|
|
7519
|
+
metadata: { sonicjs_user_id: user.userId }
|
|
7520
|
+
});
|
|
7521
|
+
return c.json({ sessionId: session.id, url: session.url });
|
|
7522
|
+
});
|
|
7523
|
+
apiRoutes3.get("/subscription", requireAuth(), async (c) => {
|
|
7524
|
+
const user = c.get("user");
|
|
7525
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
7526
|
+
const db = c.env.DB;
|
|
7527
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7528
|
+
await subscriptionService.ensureTable();
|
|
7529
|
+
const subscription = await subscriptionService.getByUserId(user.userId);
|
|
7530
|
+
if (!subscription) {
|
|
7531
|
+
return c.json({ subscription: null });
|
|
7532
|
+
}
|
|
7533
|
+
return c.json({ subscription });
|
|
7534
|
+
});
|
|
7535
|
+
apiRoutes3.get("/subscriptions", requireAuth(), async (c) => {
|
|
7536
|
+
const user = c.get("user");
|
|
7537
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7538
|
+
const db = c.env.DB;
|
|
7539
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7540
|
+
await subscriptionService.ensureTable();
|
|
7541
|
+
const filters = {
|
|
7542
|
+
status: c.req.query("status"),
|
|
7543
|
+
page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
|
|
7544
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
|
|
7545
|
+
sortBy: c.req.query("sortBy") || "created_at",
|
|
7546
|
+
sortOrder: c.req.query("sortOrder") || "desc"
|
|
7547
|
+
};
|
|
7548
|
+
const result = await subscriptionService.list(filters);
|
|
7549
|
+
return c.json(result);
|
|
7550
|
+
});
|
|
7551
|
+
apiRoutes3.get("/stats", requireAuth(), async (c) => {
|
|
7552
|
+
const user = c.get("user");
|
|
7553
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7554
|
+
const db = c.env.DB;
|
|
7555
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7556
|
+
await subscriptionService.ensureTable();
|
|
7557
|
+
const stats = await subscriptionService.getStats();
|
|
7558
|
+
return c.json(stats);
|
|
7559
|
+
});
|
|
7560
|
+
apiRoutes3.post("/sync-subscriptions", requireAuth(), async (c) => {
|
|
7561
|
+
const user = c.get("user");
|
|
7562
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7563
|
+
const db = c.env.DB;
|
|
7564
|
+
const settings = await getSettings4(db);
|
|
7565
|
+
if (!settings.stripeSecretKey) {
|
|
7566
|
+
return c.json({ error: "Stripe secret key not configured" }, 400);
|
|
7567
|
+
}
|
|
7568
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
7569
|
+
const subscriptionService = new SubscriptionService(db);
|
|
7570
|
+
await subscriptionService.ensureTable();
|
|
7571
|
+
try {
|
|
7572
|
+
const allSubs = await stripeApi.listAllSubscriptions();
|
|
7573
|
+
let synced = 0;
|
|
7574
|
+
let errors = 0;
|
|
7575
|
+
for (const sub of allSubs) {
|
|
7576
|
+
try {
|
|
7577
|
+
const userId = sub.metadata?.sonicjs_user_id || await subscriptionService.getUserIdByStripeCustomer(sub.customer) || "";
|
|
7578
|
+
await subscriptionService.upsert({
|
|
7579
|
+
userId,
|
|
7580
|
+
stripeCustomerId: typeof sub.customer === "string" ? sub.customer : sub.customer.id,
|
|
7581
|
+
stripeSubscriptionId: sub.id,
|
|
7582
|
+
stripePriceId: sub.items?.data?.[0]?.price?.id || "",
|
|
7583
|
+
status: mapStripeStatus(sub.status),
|
|
7584
|
+
currentPeriodStart: sub.current_period_start,
|
|
7585
|
+
currentPeriodEnd: sub.current_period_end,
|
|
7586
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
7587
|
+
});
|
|
7588
|
+
synced++;
|
|
7589
|
+
} catch (err) {
|
|
7590
|
+
console.error(`[Stripe Sync] Failed to upsert subscription ${sub.id}:`, err);
|
|
7591
|
+
errors++;
|
|
7592
|
+
}
|
|
7593
|
+
}
|
|
7594
|
+
return c.json({
|
|
7595
|
+
success: true,
|
|
7596
|
+
total: allSubs.length,
|
|
7597
|
+
synced,
|
|
7598
|
+
errors
|
|
7599
|
+
});
|
|
7600
|
+
} catch (error) {
|
|
7601
|
+
console.error("[Stripe Sync] Error:", error);
|
|
7602
|
+
return c.json({
|
|
7603
|
+
success: false,
|
|
7604
|
+
error: error instanceof Error ? error.message : "Sync failed"
|
|
7605
|
+
}, 500);
|
|
7606
|
+
}
|
|
7607
|
+
});
|
|
7608
|
+
apiRoutes3.get("/events", requireAuth(), async (c) => {
|
|
7609
|
+
const user = c.get("user");
|
|
7610
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
7611
|
+
const db = c.env.DB;
|
|
7612
|
+
const eventService = new StripeEventService(db);
|
|
7613
|
+
await eventService.ensureTable();
|
|
7614
|
+
const filters = {
|
|
7615
|
+
type: c.req.query("type") || void 0,
|
|
7616
|
+
status: c.req.query("status") || void 0,
|
|
7617
|
+
objectId: c.req.query("objectId") || void 0,
|
|
7618
|
+
page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
|
|
7619
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50
|
|
7620
|
+
};
|
|
7621
|
+
const [result, stats, types] = await Promise.all([
|
|
7622
|
+
eventService.list(filters),
|
|
7623
|
+
eventService.getStats(),
|
|
7624
|
+
eventService.getDistinctTypes()
|
|
7625
|
+
]);
|
|
7626
|
+
return c.json({ ...result, stats, types });
|
|
7627
|
+
});
|
|
7628
|
+
|
|
7629
|
+
// src/plugins/core-plugins/stripe-plugin/index.ts
|
|
7630
|
+
function createStripePlugin() {
|
|
7631
|
+
const builder = PluginBuilder.create({
|
|
7632
|
+
name: "stripe",
|
|
7633
|
+
version: "1.0.0-beta.1",
|
|
7634
|
+
description: "Stripe subscription management with webhook handling, checkout sessions, and subscription gating"
|
|
7635
|
+
});
|
|
7636
|
+
builder.metadata({
|
|
7637
|
+
author: { name: "SonicJS Team" },
|
|
7638
|
+
license: "MIT"
|
|
7639
|
+
});
|
|
7640
|
+
builder.addRoute("/admin/plugins/stripe", adminRoutes3, {
|
|
7641
|
+
description: "Stripe subscriptions admin dashboard",
|
|
7642
|
+
requiresAuth: true,
|
|
7643
|
+
priority: 50
|
|
7644
|
+
});
|
|
7645
|
+
builder.addRoute("/api/stripe", apiRoutes3, {
|
|
7646
|
+
description: "Stripe API endpoints (webhook, checkout, subscription)",
|
|
7647
|
+
requiresAuth: false,
|
|
7648
|
+
// Webhook route handles its own auth via signature
|
|
7649
|
+
priority: 50
|
|
7650
|
+
});
|
|
7651
|
+
builder.addMenuItem("Stripe", "/admin/plugins/stripe", {
|
|
7652
|
+
icon: `<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 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>`,
|
|
7653
|
+
order: 75
|
|
7654
|
+
});
|
|
7655
|
+
builder.lifecycle({
|
|
7656
|
+
install: async () => {
|
|
7657
|
+
console.log("[Stripe] Plugin installed");
|
|
7658
|
+
},
|
|
7659
|
+
activate: async () => {
|
|
7660
|
+
console.log("[Stripe] Plugin activated");
|
|
7661
|
+
},
|
|
7662
|
+
deactivate: async () => {
|
|
7663
|
+
console.log("[Stripe] Plugin deactivated");
|
|
7664
|
+
},
|
|
7665
|
+
uninstall: async () => {
|
|
7666
|
+
console.log("[Stripe] Plugin uninstalled");
|
|
7667
|
+
}
|
|
7668
|
+
});
|
|
7669
|
+
return builder.build();
|
|
7670
|
+
}
|
|
7671
|
+
var stripePlugin = createStripePlugin();
|
|
7672
|
+
|
|
6274
7673
|
// src/middleware/plugin-menu.ts
|
|
6275
|
-
var
|
|
6276
|
-
|
|
6277
|
-
|
|
7674
|
+
var REGISTRY_MENU_PLUGINS = Object.values(PLUGIN_REGISTRY).filter((p) => p.adminMenu !== null).map((p) => ({
|
|
7675
|
+
codeName: p.codeName,
|
|
7676
|
+
label: p.adminMenu.label,
|
|
7677
|
+
path: p.adminMenu.path,
|
|
7678
|
+
icon: p.adminMenu.icon,
|
|
7679
|
+
order: p.adminMenu.order
|
|
7680
|
+
}));
|
|
7681
|
+
var ICON_SVG = {
|
|
7682
|
+
"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>',
|
|
7683
|
+
"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>',
|
|
7684
|
+
"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>',
|
|
7685
|
+
"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>',
|
|
7686
|
+
"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>',
|
|
7687
|
+
"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>',
|
|
7688
|
+
"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>',
|
|
7689
|
+
"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>',
|
|
7690
|
+
"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>',
|
|
7691
|
+
"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>'
|
|
7692
|
+
};
|
|
7693
|
+
function resolveIcon(iconName) {
|
|
7694
|
+
if (!iconName) return "";
|
|
7695
|
+
if (iconName.startsWith("<svg") || iconName.startsWith("<")) return iconName;
|
|
7696
|
+
return ICON_SVG[iconName] || "";
|
|
7697
|
+
}
|
|
6278
7698
|
var MARKER = "<!-- DYNAMIC_PLUGIN_MENU -->";
|
|
6279
7699
|
function renderMenuItem(item, currentPath) {
|
|
6280
7700
|
const isActive = currentPath === item.path || currentPath.startsWith(item.path);
|
|
6281
7701
|
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>`;
|
|
7702
|
+
const resolvedIcon = resolveIcon(item.icon) || fallbackIcon;
|
|
6282
7703
|
return `
|
|
6283
7704
|
<span class="relative">
|
|
6284
7705
|
${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
|
|
@@ -6288,7 +7709,7 @@ function renderMenuItem(item, currentPath) {
|
|
|
6288
7709
|
${isActive ? 'data-current="true"' : ""}
|
|
6289
7710
|
>
|
|
6290
7711
|
<span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">
|
|
6291
|
-
${
|
|
7712
|
+
${resolvedIcon}
|
|
6292
7713
|
</span>
|
|
6293
7714
|
<span class="truncate">${item.label}</span>
|
|
6294
7715
|
</a>
|
|
@@ -6303,23 +7724,28 @@ function pluginMenuMiddleware() {
|
|
|
6303
7724
|
let activeMenuItems = [];
|
|
6304
7725
|
try {
|
|
6305
7726
|
const db = c.env.DB;
|
|
6306
|
-
const
|
|
6307
|
-
if (
|
|
6308
|
-
const placeholders =
|
|
7727
|
+
const pluginCodeNames = REGISTRY_MENU_PLUGINS.map((p) => p.codeName);
|
|
7728
|
+
if (pluginCodeNames.length > 0) {
|
|
7729
|
+
const placeholders = pluginCodeNames.map(() => "?").join(",");
|
|
6309
7730
|
const result = await db.prepare(
|
|
6310
7731
|
`SELECT name FROM plugins WHERE name IN (${placeholders}) AND status = 'active'`
|
|
6311
|
-
).bind(...
|
|
7732
|
+
).bind(...pluginCodeNames).all();
|
|
6312
7733
|
const activeNames = new Set((result.results || []).map((r) => r.name));
|
|
6313
|
-
for (const plugin2 of
|
|
6314
|
-
if (activeNames.has(plugin2.
|
|
6315
|
-
activeMenuItems.push(
|
|
7734
|
+
for (const plugin2 of REGISTRY_MENU_PLUGINS) {
|
|
7735
|
+
if (activeNames.has(plugin2.codeName)) {
|
|
7736
|
+
activeMenuItems.push({
|
|
7737
|
+
label: plugin2.label,
|
|
7738
|
+
path: plugin2.path,
|
|
7739
|
+
icon: plugin2.icon,
|
|
7740
|
+
order: plugin2.order
|
|
7741
|
+
});
|
|
6316
7742
|
}
|
|
6317
7743
|
}
|
|
6318
|
-
activeMenuItems.sort((a, b) =>
|
|
7744
|
+
activeMenuItems.sort((a, b) => a.order - b.order);
|
|
6319
7745
|
}
|
|
6320
7746
|
} catch {
|
|
6321
7747
|
}
|
|
6322
|
-
c.set("pluginMenuItems", activeMenuItems.map((m) => ({ label: m.label, path: m.path, icon: m.icon || "" })));
|
|
7748
|
+
c.set("pluginMenuItems", activeMenuItems.map((m) => ({ label: m.label, path: m.path, icon: resolveIcon(m.icon) || "" })));
|
|
6323
7749
|
await next();
|
|
6324
7750
|
if (activeMenuItems.length > 0 && c.res.headers.get("content-type")?.includes("text/html")) {
|
|
6325
7751
|
const status = c.res.status;
|
|
@@ -8251,6 +9677,11 @@ function createSonicJSApp(config = {}) {
|
|
|
8251
9677
|
app2.route(route.path, route.handler);
|
|
8252
9678
|
}
|
|
8253
9679
|
}
|
|
9680
|
+
if (stripePlugin.routes && stripePlugin.routes.length > 0) {
|
|
9681
|
+
for (const route of stripePlugin.routes) {
|
|
9682
|
+
app2.route(route.path, route.handler);
|
|
9683
|
+
}
|
|
9684
|
+
}
|
|
8254
9685
|
app2.route("/admin/plugins", adminPluginRoutes);
|
|
8255
9686
|
app2.route("/admin/logs", adminLogsRoutes);
|
|
8256
9687
|
app2.route("/admin", userRoutes);
|