@sonicjs-cms/core 2.12.1 → 2.14.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/{chunk-3V2CQFIR.js → chunk-23DP6TO5.js} +89 -49
- package/dist/chunk-23DP6TO5.js.map +1 -0
- package/dist/{chunk-RBXFXT7H.cjs → chunk-3QCEYJLK.cjs} +9 -9
- package/dist/{chunk-RBXFXT7H.cjs.map → chunk-3QCEYJLK.cjs.map} +1 -1
- package/dist/{chunk-UFWE3MEJ.js → chunk-AFGOH2F6.js} +3 -3
- package/dist/{chunk-UFWE3MEJ.js.map → chunk-AFGOH2F6.js.map} +1 -1
- package/dist/{chunk-BWZBKLOC.js → chunk-CB7ONLGB.js} +3 -3
- package/dist/{chunk-BWZBKLOC.js.map → chunk-CB7ONLGB.js.map} +1 -1
- package/dist/{chunk-DHTCZZUB.cjs → chunk-DRWSHIFG.cjs} +252 -212
- package/dist/chunk-DRWSHIFG.cjs.map +1 -0
- package/dist/{chunk-673VROB3.js → chunk-GAVTTYMC.js} +5 -5
- package/dist/{chunk-673VROB3.js.map → chunk-GAVTTYMC.js.map} +1 -1
- package/dist/{chunk-6C6W54QP.js → chunk-JKNKO6LA.js} +22 -5
- package/dist/chunk-JKNKO6LA.js.map +1 -0
- package/dist/{chunk-76TX6XND.js → chunk-JTUCC6WZ.js} +18 -10
- package/dist/chunk-JTUCC6WZ.js.map +1 -0
- package/dist/{chunk-XK3TKOLQ.cjs → chunk-KZ2MFGET.cjs} +22 -5
- package/dist/chunk-KZ2MFGET.cjs.map +1 -0
- package/dist/{chunk-H4NHRZ6Y.cjs → chunk-QP3OHHON.cjs} +18 -10
- package/dist/chunk-QP3OHHON.cjs.map +1 -0
- package/dist/{chunk-IKBAY2M2.cjs → chunk-YULUPQZV.cjs} +5 -5
- package/dist/{chunk-IKBAY2M2.cjs.map → chunk-YULUPQZV.cjs.map} +1 -1
- package/dist/{chunk-HBUFGLEX.cjs → chunk-YYMPHM3I.cjs} +4 -4
- package/dist/{chunk-HBUFGLEX.cjs.map → chunk-YYMPHM3I.cjs.map} +1 -1
- package/dist/index.cjs +887 -121
- 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 +779 -13
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +28 -28
- package/dist/middleware.js +2 -2
- package/dist/migrations-F7KVA74T.cjs +13 -0
- package/dist/{migrations-MIZFGFNS.cjs.map → migrations-F7KVA74T.cjs.map} +1 -1
- package/dist/migrations-WKONKRN7.js +4 -0
- package/dist/{migrations-AH2XIFSA.js.map → migrations-WKONKRN7.js.map} +1 -1
- package/dist/{plugin-bootstrap-DVGLQrcO.d.cts → plugin-bootstrap-BGwBraaN.d.cts} +1 -0
- package/dist/{plugin-bootstrap-CZ1GDum7.d.ts → plugin-bootstrap-Drns7X9w.d.ts} +1 -0
- package/dist/routes.cjs +28 -28
- package/dist/routes.js +5 -5
- package/dist/services.cjs +2 -2
- package/dist/services.d.cts +1 -1
- package/dist/services.d.ts +1 -1
- package/dist/services.js +1 -1
- package/dist/templates.cjs +19 -19
- package/dist/templates.js +2 -2
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-3V2CQFIR.js.map +0 -1
- package/dist/chunk-6C6W54QP.js.map +0 -1
- package/dist/chunk-76TX6XND.js.map +0 -1
- package/dist/chunk-DHTCZZUB.cjs.map +0 -1
- package/dist/chunk-H4NHRZ6Y.cjs.map +0 -1
- package/dist/chunk-XK3TKOLQ.cjs.map +0 -1
- package/dist/migrations-AH2XIFSA.js +0 -4
- package/dist/migrations-MIZFGFNS.cjs +0 -13
package/dist/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
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-23DP6TO5.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-23DP6TO5.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-
|
|
5
|
+
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-AFGOH2F6.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-AFGOH2F6.js';
|
|
7
7
|
import { PluginService } from './chunk-H3XXBAMO.js';
|
|
8
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-H3XXBAMO.js';
|
|
9
|
-
export { MigrationService } from './chunk-
|
|
10
|
-
export { renderFilterBar } from './chunk-
|
|
11
|
-
import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-
|
|
12
|
-
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-
|
|
9
|
+
export { MigrationService } from './chunk-JKNKO6LA.js';
|
|
10
|
+
export { renderFilterBar } from './chunk-CB7ONLGB.js';
|
|
11
|
+
import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-JTUCC6WZ.js';
|
|
12
|
+
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-JTUCC6WZ.js';
|
|
13
13
|
export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-2MXF4RYZ.js';
|
|
14
14
|
import { PluginBuilder } from './chunk-J5WGMRSU.js';
|
|
15
15
|
export { PluginBuilder, PluginHelpers } from './chunk-J5WGMRSU.js';
|
|
16
|
-
import { package_default, getCoreVersion } from './chunk-
|
|
17
|
-
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-
|
|
16
|
+
import { package_default, getCoreVersion } from './chunk-GAVTTYMC.js';
|
|
17
|
+
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-GAVTTYMC.js';
|
|
18
18
|
import './chunk-X7ZAEI5S.js';
|
|
19
19
|
export { metricsTracker } from './chunk-FICTAGD4.js';
|
|
20
20
|
export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
|
|
@@ -1916,11 +1916,23 @@ function createOTPLoginPlugin() {
|
|
|
1916
1916
|
attemptsRemaining: verification.attemptsRemaining
|
|
1917
1917
|
}, 401);
|
|
1918
1918
|
}
|
|
1919
|
-
|
|
1919
|
+
let user = await db.prepare(`
|
|
1920
1920
|
SELECT id, email, role, is_active
|
|
1921
1921
|
FROM users
|
|
1922
1922
|
WHERE email = ?
|
|
1923
1923
|
`).bind(normalizedEmail).first();
|
|
1924
|
+
if (!user && settings.allowNewUserRegistration) {
|
|
1925
|
+
const userId = crypto.randomUUID();
|
|
1926
|
+
const now = Date.now();
|
|
1927
|
+
const username = normalizedEmail.split("@")[0] + "_" + userId.slice(0, 6);
|
|
1928
|
+
await db.prepare(`
|
|
1929
|
+
INSERT INTO users (
|
|
1930
|
+
id, email, username, first_name, last_name,
|
|
1931
|
+
password_hash, role, is_active, email_verified, created_at, updated_at
|
|
1932
|
+
) VALUES (?, ?, ?, '', '', NULL, 'viewer', 1, 1, ?, ?)
|
|
1933
|
+
`).bind(userId, normalizedEmail, username, now, now).run();
|
|
1934
|
+
user = { id: userId, email: normalizedEmail, role: "viewer", is_active: 1 };
|
|
1935
|
+
}
|
|
1924
1936
|
if (!user) {
|
|
1925
1937
|
return c.json({
|
|
1926
1938
|
error: "User not found"
|
|
@@ -3889,7 +3901,7 @@ function renderSettingsPage(data) {
|
|
|
3889
3901
|
const indexStatusMap = data.indexStatus || {};
|
|
3890
3902
|
const status = indexStatusMap[collectionId];
|
|
3891
3903
|
const isNew = collection.is_new === true && !isDismissed && !status;
|
|
3892
|
-
const
|
|
3904
|
+
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>` : "";
|
|
3893
3905
|
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"}">
|
|
3894
3906
|
<input
|
|
3895
3907
|
type="checkbox"
|
|
@@ -3904,7 +3916,7 @@ function renderSettingsPage(data) {
|
|
|
3904
3916
|
<label for="collection_${collectionId}" class="text-sm font-medium text-zinc-950 dark:text-white select-none cursor-pointer flex items-center">
|
|
3905
3917
|
${collection.display_name || collection.name || "Unnamed Collection"}
|
|
3906
3918
|
${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>' : ""}
|
|
3907
|
-
${
|
|
3919
|
+
${statusBadge2}
|
|
3908
3920
|
</label>
|
|
3909
3921
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
|
3910
3922
|
${collection.description || collection.name || "No description"} \u2022 ${collection.item_count || 0} items
|
|
@@ -6259,6 +6271,755 @@ function createSecurityAuditPlugin() {
|
|
|
6259
6271
|
}
|
|
6260
6272
|
var securityAuditPlugin = createSecurityAuditPlugin();
|
|
6261
6273
|
|
|
6274
|
+
// src/plugins/core-plugins/stripe-plugin/services/subscription-service.ts
|
|
6275
|
+
var SubscriptionService = class {
|
|
6276
|
+
constructor(db) {
|
|
6277
|
+
this.db = db;
|
|
6278
|
+
}
|
|
6279
|
+
/**
|
|
6280
|
+
* Ensure the subscriptions table exists
|
|
6281
|
+
*/
|
|
6282
|
+
async ensureTable() {
|
|
6283
|
+
await this.db.prepare(`
|
|
6284
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
6285
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
6286
|
+
user_id TEXT NOT NULL,
|
|
6287
|
+
stripe_customer_id TEXT NOT NULL,
|
|
6288
|
+
stripe_subscription_id TEXT NOT NULL UNIQUE,
|
|
6289
|
+
stripe_price_id TEXT NOT NULL,
|
|
6290
|
+
status TEXT NOT NULL DEFAULT 'incomplete',
|
|
6291
|
+
current_period_start INTEGER NOT NULL DEFAULT 0,
|
|
6292
|
+
current_period_end INTEGER NOT NULL DEFAULT 0,
|
|
6293
|
+
cancel_at_period_end INTEGER NOT NULL DEFAULT 0,
|
|
6294
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
6295
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
6296
|
+
)
|
|
6297
|
+
`).run();
|
|
6298
|
+
await this.db.prepare(`
|
|
6299
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id)
|
|
6300
|
+
`).run();
|
|
6301
|
+
await this.db.prepare(`
|
|
6302
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id)
|
|
6303
|
+
`).run();
|
|
6304
|
+
await this.db.prepare(`
|
|
6305
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id)
|
|
6306
|
+
`).run();
|
|
6307
|
+
await this.db.prepare(`
|
|
6308
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status)
|
|
6309
|
+
`).run();
|
|
6310
|
+
}
|
|
6311
|
+
/**
|
|
6312
|
+
* Create a new subscription record
|
|
6313
|
+
*/
|
|
6314
|
+
async create(data) {
|
|
6315
|
+
const result = await this.db.prepare(`
|
|
6316
|
+
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)
|
|
6317
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6318
|
+
RETURNING *
|
|
6319
|
+
`).bind(
|
|
6320
|
+
data.userId,
|
|
6321
|
+
data.stripeCustomerId,
|
|
6322
|
+
data.stripeSubscriptionId,
|
|
6323
|
+
data.stripePriceId,
|
|
6324
|
+
data.status,
|
|
6325
|
+
data.currentPeriodStart,
|
|
6326
|
+
data.currentPeriodEnd,
|
|
6327
|
+
data.cancelAtPeriodEnd ? 1 : 0
|
|
6328
|
+
).first();
|
|
6329
|
+
return this.mapRow(result);
|
|
6330
|
+
}
|
|
6331
|
+
/**
|
|
6332
|
+
* Update a subscription by its Stripe subscription ID
|
|
6333
|
+
*/
|
|
6334
|
+
async updateByStripeId(stripeSubscriptionId, data) {
|
|
6335
|
+
const sets = [];
|
|
6336
|
+
const values = [];
|
|
6337
|
+
if (data.status !== void 0) {
|
|
6338
|
+
sets.push("status = ?");
|
|
6339
|
+
values.push(data.status);
|
|
6340
|
+
}
|
|
6341
|
+
if (data.stripePriceId !== void 0) {
|
|
6342
|
+
sets.push("stripe_price_id = ?");
|
|
6343
|
+
values.push(data.stripePriceId);
|
|
6344
|
+
}
|
|
6345
|
+
if (data.currentPeriodStart !== void 0) {
|
|
6346
|
+
sets.push("current_period_start = ?");
|
|
6347
|
+
values.push(data.currentPeriodStart);
|
|
6348
|
+
}
|
|
6349
|
+
if (data.currentPeriodEnd !== void 0) {
|
|
6350
|
+
sets.push("current_period_end = ?");
|
|
6351
|
+
values.push(data.currentPeriodEnd);
|
|
6352
|
+
}
|
|
6353
|
+
if (data.cancelAtPeriodEnd !== void 0) {
|
|
6354
|
+
sets.push("cancel_at_period_end = ?");
|
|
6355
|
+
values.push(data.cancelAtPeriodEnd ? 1 : 0);
|
|
6356
|
+
}
|
|
6357
|
+
if (sets.length === 0) return this.getByStripeSubscriptionId(stripeSubscriptionId);
|
|
6358
|
+
sets.push("updated_at = unixepoch()");
|
|
6359
|
+
values.push(stripeSubscriptionId);
|
|
6360
|
+
const result = await this.db.prepare(`
|
|
6361
|
+
UPDATE subscriptions SET ${sets.join(", ")} WHERE stripe_subscription_id = ? RETURNING *
|
|
6362
|
+
`).bind(...values).first();
|
|
6363
|
+
return result ? this.mapRow(result) : null;
|
|
6364
|
+
}
|
|
6365
|
+
/**
|
|
6366
|
+
* Get subscription by Stripe subscription ID
|
|
6367
|
+
*/
|
|
6368
|
+
async getByStripeSubscriptionId(stripeSubscriptionId) {
|
|
6369
|
+
const result = await this.db.prepare(
|
|
6370
|
+
"SELECT * FROM subscriptions WHERE stripe_subscription_id = ?"
|
|
6371
|
+
).bind(stripeSubscriptionId).first();
|
|
6372
|
+
return result ? this.mapRow(result) : null;
|
|
6373
|
+
}
|
|
6374
|
+
/**
|
|
6375
|
+
* Get the active subscription for a user
|
|
6376
|
+
*/
|
|
6377
|
+
async getByUserId(userId) {
|
|
6378
|
+
const result = await this.db.prepare(
|
|
6379
|
+
"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"
|
|
6380
|
+
).bind(userId).first();
|
|
6381
|
+
return result ? this.mapRow(result) : null;
|
|
6382
|
+
}
|
|
6383
|
+
/**
|
|
6384
|
+
* Get subscription by Stripe customer ID
|
|
6385
|
+
*/
|
|
6386
|
+
async getByStripeCustomerId(stripeCustomerId) {
|
|
6387
|
+
const result = await this.db.prepare(
|
|
6388
|
+
"SELECT * FROM subscriptions WHERE stripe_customer_id = ? ORDER BY updated_at DESC LIMIT 1"
|
|
6389
|
+
).bind(stripeCustomerId).first();
|
|
6390
|
+
return result ? this.mapRow(result) : null;
|
|
6391
|
+
}
|
|
6392
|
+
/**
|
|
6393
|
+
* Find the userId linked to a Stripe customer ID
|
|
6394
|
+
*/
|
|
6395
|
+
async getUserIdByStripeCustomer(stripeCustomerId) {
|
|
6396
|
+
const result = await this.db.prepare(
|
|
6397
|
+
"SELECT user_id FROM subscriptions WHERE stripe_customer_id = ? LIMIT 1"
|
|
6398
|
+
).bind(stripeCustomerId).first();
|
|
6399
|
+
return result?.user_id ?? null;
|
|
6400
|
+
}
|
|
6401
|
+
/**
|
|
6402
|
+
* List subscriptions with filters and pagination
|
|
6403
|
+
*/
|
|
6404
|
+
async list(filters = {}) {
|
|
6405
|
+
const where = [];
|
|
6406
|
+
const values = [];
|
|
6407
|
+
if (filters.status) {
|
|
6408
|
+
where.push("status = ?");
|
|
6409
|
+
values.push(filters.status);
|
|
6410
|
+
}
|
|
6411
|
+
if (filters.userId) {
|
|
6412
|
+
where.push("user_id = ?");
|
|
6413
|
+
values.push(filters.userId);
|
|
6414
|
+
}
|
|
6415
|
+
if (filters.stripeCustomerId) {
|
|
6416
|
+
where.push("stripe_customer_id = ?");
|
|
6417
|
+
values.push(filters.stripeCustomerId);
|
|
6418
|
+
}
|
|
6419
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
6420
|
+
const sortBy = filters.sortBy || "created_at";
|
|
6421
|
+
const sortOrder = filters.sortOrder || "desc";
|
|
6422
|
+
const limit = Math.min(filters.limit || 50, 100);
|
|
6423
|
+
const page = filters.page || 1;
|
|
6424
|
+
const offset = (page - 1) * limit;
|
|
6425
|
+
const countResult = await this.db.prepare(
|
|
6426
|
+
`SELECT COUNT(*) as count FROM subscriptions ${whereClause}`
|
|
6427
|
+
).bind(...values).first();
|
|
6428
|
+
const results = await this.db.prepare(
|
|
6429
|
+
`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 ?`
|
|
6430
|
+
).bind(...values, limit, offset).all();
|
|
6431
|
+
return {
|
|
6432
|
+
subscriptions: (results.results || []).map((r) => this.mapRow(r)),
|
|
6433
|
+
total: countResult?.count || 0
|
|
6434
|
+
};
|
|
6435
|
+
}
|
|
6436
|
+
/**
|
|
6437
|
+
* Get subscription stats
|
|
6438
|
+
*/
|
|
6439
|
+
async getStats() {
|
|
6440
|
+
const result = await this.db.prepare(`
|
|
6441
|
+
SELECT
|
|
6442
|
+
COUNT(*) as total,
|
|
6443
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
6444
|
+
SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) as canceled,
|
|
6445
|
+
SUM(CASE WHEN status = 'past_due' THEN 1 ELSE 0 END) as past_due,
|
|
6446
|
+
SUM(CASE WHEN status = 'trialing' THEN 1 ELSE 0 END) as trialing
|
|
6447
|
+
FROM subscriptions
|
|
6448
|
+
`).first();
|
|
6449
|
+
return {
|
|
6450
|
+
total: result?.total || 0,
|
|
6451
|
+
active: result?.active || 0,
|
|
6452
|
+
canceled: result?.canceled || 0,
|
|
6453
|
+
pastDue: result?.past_due || 0,
|
|
6454
|
+
trialing: result?.trialing || 0
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6457
|
+
/**
|
|
6458
|
+
* Delete a subscription record by Stripe subscription ID
|
|
6459
|
+
*/
|
|
6460
|
+
async deleteByStripeId(stripeSubscriptionId) {
|
|
6461
|
+
const result = await this.db.prepare(
|
|
6462
|
+
"DELETE FROM subscriptions WHERE stripe_subscription_id = ?"
|
|
6463
|
+
).bind(stripeSubscriptionId).run();
|
|
6464
|
+
return (result.meta?.changes || 0) > 0;
|
|
6465
|
+
}
|
|
6466
|
+
mapRow(row) {
|
|
6467
|
+
return {
|
|
6468
|
+
id: row.id,
|
|
6469
|
+
userId: row.user_id,
|
|
6470
|
+
stripeCustomerId: row.stripe_customer_id,
|
|
6471
|
+
stripeSubscriptionId: row.stripe_subscription_id,
|
|
6472
|
+
stripePriceId: row.stripe_price_id,
|
|
6473
|
+
status: row.status,
|
|
6474
|
+
currentPeriodStart: row.current_period_start,
|
|
6475
|
+
currentPeriodEnd: row.current_period_end,
|
|
6476
|
+
cancelAtPeriodEnd: !!row.cancel_at_period_end,
|
|
6477
|
+
createdAt: row.created_at,
|
|
6478
|
+
updatedAt: row.updated_at,
|
|
6479
|
+
// Attach email if joined
|
|
6480
|
+
...row.user_email ? { userEmail: row.user_email } : {}
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
};
|
|
6484
|
+
|
|
6485
|
+
// src/plugins/core-plugins/stripe-plugin/components/subscriptions-page.ts
|
|
6486
|
+
function renderSubscriptionsPage(subscriptions, stats, filters) {
|
|
6487
|
+
return `
|
|
6488
|
+
<div class="space-y-6">
|
|
6489
|
+
<!-- Stats Cards -->
|
|
6490
|
+
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
6491
|
+
${statsCard("Total", stats.total, "text-gray-700")}
|
|
6492
|
+
${statsCard("Active", stats.active, "text-green-600")}
|
|
6493
|
+
${statsCard("Trialing", stats.trialing, "text-blue-600")}
|
|
6494
|
+
${statsCard("Past Due", stats.pastDue, "text-yellow-600")}
|
|
6495
|
+
${statsCard("Canceled", stats.canceled, "text-red-600")}
|
|
6496
|
+
</div>
|
|
6497
|
+
|
|
6498
|
+
<!-- Filters -->
|
|
6499
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
6500
|
+
<form method="GET" class="flex items-center gap-4">
|
|
6501
|
+
<label class="text-sm font-medium text-gray-700">Status:</label>
|
|
6502
|
+
<select name="status" class="border rounded px-3 py-1.5 text-sm" onchange="this.form.submit()">
|
|
6503
|
+
<option value="">All</option>
|
|
6504
|
+
${statusOption("active", filters.status)}
|
|
6505
|
+
${statusOption("trialing", filters.status)}
|
|
6506
|
+
${statusOption("past_due", filters.status)}
|
|
6507
|
+
${statusOption("canceled", filters.status)}
|
|
6508
|
+
${statusOption("unpaid", filters.status)}
|
|
6509
|
+
${statusOption("paused", filters.status)}
|
|
6510
|
+
</select>
|
|
6511
|
+
</form>
|
|
6512
|
+
</div>
|
|
6513
|
+
|
|
6514
|
+
<!-- Subscriptions Table -->
|
|
6515
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
6516
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
6517
|
+
<thead class="bg-gray-50">
|
|
6518
|
+
<tr>
|
|
6519
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
|
|
6520
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
6521
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price ID</th>
|
|
6522
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current Period</th>
|
|
6523
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cancel at End</th>
|
|
6524
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Stripe</th>
|
|
6525
|
+
</tr>
|
|
6526
|
+
</thead>
|
|
6527
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
6528
|
+
${subscriptions.length === 0 ? '<tr><td colspan="6" class="px-6 py-8 text-center text-gray-500">No subscriptions found</td></tr>' : subscriptions.map(renderRow).join("")}
|
|
6529
|
+
</tbody>
|
|
6530
|
+
</table>
|
|
6531
|
+
|
|
6532
|
+
${renderPagination2(filters.page, filters.totalPages, filters.status)}
|
|
6533
|
+
</div>
|
|
6534
|
+
</div>
|
|
6535
|
+
`;
|
|
6536
|
+
}
|
|
6537
|
+
function statsCard(label, value, colorClass) {
|
|
6538
|
+
return `
|
|
6539
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
6540
|
+
<div class="text-sm font-medium text-gray-500">${label}</div>
|
|
6541
|
+
<div class="text-2xl font-bold ${colorClass}">${value}</div>
|
|
6542
|
+
</div>
|
|
6543
|
+
`;
|
|
6544
|
+
}
|
|
6545
|
+
function statusOption(value, current) {
|
|
6546
|
+
const selected = value === current ? "selected" : "";
|
|
6547
|
+
const label = value.replace("_", " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
6548
|
+
return `<option value="${value}" ${selected}>${label}</option>`;
|
|
6549
|
+
}
|
|
6550
|
+
function statusBadge(status) {
|
|
6551
|
+
const colors = {
|
|
6552
|
+
active: "bg-green-100 text-green-800",
|
|
6553
|
+
trialing: "bg-blue-100 text-blue-800",
|
|
6554
|
+
past_due: "bg-yellow-100 text-yellow-800",
|
|
6555
|
+
canceled: "bg-red-100 text-red-800",
|
|
6556
|
+
unpaid: "bg-orange-100 text-orange-800",
|
|
6557
|
+
paused: "bg-gray-100 text-gray-800",
|
|
6558
|
+
incomplete: "bg-gray-100 text-gray-500",
|
|
6559
|
+
incomplete_expired: "bg-red-100 text-red-500"
|
|
6560
|
+
};
|
|
6561
|
+
const color = colors[status] || "bg-gray-100 text-gray-800";
|
|
6562
|
+
const label = status.replace("_", " ");
|
|
6563
|
+
return `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${color}">${label}</span>`;
|
|
6564
|
+
}
|
|
6565
|
+
function formatDate(timestamp) {
|
|
6566
|
+
if (!timestamp) return "-";
|
|
6567
|
+
return new Date(timestamp * 1e3).toLocaleDateString("en-US", {
|
|
6568
|
+
month: "short",
|
|
6569
|
+
day: "numeric",
|
|
6570
|
+
year: "numeric"
|
|
6571
|
+
});
|
|
6572
|
+
}
|
|
6573
|
+
function renderRow(sub) {
|
|
6574
|
+
return `
|
|
6575
|
+
<tr>
|
|
6576
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6577
|
+
<div class="text-sm font-medium text-gray-900">${sub.userEmail || sub.userId}</div>
|
|
6578
|
+
<div class="text-xs text-gray-500">${sub.stripeCustomerId}</div>
|
|
6579
|
+
</td>
|
|
6580
|
+
<td class="px-6 py-4 whitespace-nowrap">${statusBadge(sub.status)}</td>
|
|
6581
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${sub.stripePriceId}</td>
|
|
6582
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
6583
|
+
${formatDate(sub.currentPeriodStart)} - ${formatDate(sub.currentPeriodEnd)}
|
|
6584
|
+
</td>
|
|
6585
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6586
|
+
${sub.cancelAtPeriodEnd ? '<span class="text-yellow-600 font-medium">Yes</span>' : '<span class="text-gray-400">No</span>'}
|
|
6587
|
+
</td>
|
|
6588
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
6589
|
+
<a href="https://dashboard.stripe.com/subscriptions/${sub.stripeSubscriptionId}"
|
|
6590
|
+
target="_blank" rel="noopener noreferrer"
|
|
6591
|
+
class="text-indigo-600 hover:text-indigo-800">
|
|
6592
|
+
View in Stripe
|
|
6593
|
+
</a>
|
|
6594
|
+
</td>
|
|
6595
|
+
</tr>
|
|
6596
|
+
`;
|
|
6597
|
+
}
|
|
6598
|
+
function renderPagination2(page, totalPages, status) {
|
|
6599
|
+
if (totalPages <= 1) return "";
|
|
6600
|
+
const params = status ? `&status=${status}` : "";
|
|
6601
|
+
return `
|
|
6602
|
+
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
|
6603
|
+
<div class="text-sm text-gray-700">
|
|
6604
|
+
Page ${page} of ${totalPages}
|
|
6605
|
+
</div>
|
|
6606
|
+
<div class="flex gap-2">
|
|
6607
|
+
${page > 1 ? `<a href="?page=${page - 1}${params}" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">Previous</a>` : ""}
|
|
6608
|
+
${page < totalPages ? `<a href="?page=${page + 1}${params}" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">Next</a>` : ""}
|
|
6609
|
+
</div>
|
|
6610
|
+
</div>
|
|
6611
|
+
`;
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
// src/plugins/core-plugins/stripe-plugin/types.ts
|
|
6615
|
+
var DEFAULT_SETTINGS3 = {
|
|
6616
|
+
stripeSecretKey: "",
|
|
6617
|
+
stripeWebhookSecret: "",
|
|
6618
|
+
stripePriceId: "",
|
|
6619
|
+
successUrl: "/admin/dashboard",
|
|
6620
|
+
cancelUrl: "/admin/dashboard"
|
|
6621
|
+
};
|
|
6622
|
+
|
|
6623
|
+
// src/plugins/core-plugins/stripe-plugin/routes/admin.ts
|
|
6624
|
+
var adminRoutes3 = new Hono();
|
|
6625
|
+
adminRoutes3.use("*", requireAuth());
|
|
6626
|
+
adminRoutes3.use("*", async (c, next) => {
|
|
6627
|
+
const user = c.get("user");
|
|
6628
|
+
if (user?.role !== "admin") {
|
|
6629
|
+
return c.text("Access denied", 403);
|
|
6630
|
+
}
|
|
6631
|
+
return next();
|
|
6632
|
+
});
|
|
6633
|
+
adminRoutes3.get("/", async (c) => {
|
|
6634
|
+
const db = c.env.DB;
|
|
6635
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6636
|
+
await subscriptionService.ensureTable();
|
|
6637
|
+
const page = parseInt(c.req.query("page") || "1");
|
|
6638
|
+
const limit = 50;
|
|
6639
|
+
const statusFilter = c.req.query("status");
|
|
6640
|
+
const [{ subscriptions, total }, stats] = await Promise.all([
|
|
6641
|
+
subscriptionService.list({ status: statusFilter, page, limit }),
|
|
6642
|
+
subscriptionService.getStats()
|
|
6643
|
+
]);
|
|
6644
|
+
const totalPages = Math.ceil(total / limit);
|
|
6645
|
+
const html = renderSubscriptionsPage(subscriptions, stats, {
|
|
6646
|
+
status: statusFilter,
|
|
6647
|
+
page,
|
|
6648
|
+
totalPages
|
|
6649
|
+
});
|
|
6650
|
+
return c.html(html);
|
|
6651
|
+
});
|
|
6652
|
+
adminRoutes3.post("/settings", async (c) => {
|
|
6653
|
+
try {
|
|
6654
|
+
const body = await c.req.json();
|
|
6655
|
+
const db = c.env.DB;
|
|
6656
|
+
await db.prepare(`
|
|
6657
|
+
UPDATE plugins
|
|
6658
|
+
SET settings = ?,
|
|
6659
|
+
updated_at = unixepoch()
|
|
6660
|
+
WHERE id = 'stripe'
|
|
6661
|
+
`).bind(JSON.stringify(body)).run();
|
|
6662
|
+
return c.json({ success: true });
|
|
6663
|
+
} catch (error) {
|
|
6664
|
+
console.error("Error saving Stripe settings:", error);
|
|
6665
|
+
return c.json({ success: false, error: "Failed to save settings" }, 500);
|
|
6666
|
+
}
|
|
6667
|
+
});
|
|
6668
|
+
|
|
6669
|
+
// src/plugins/core-plugins/stripe-plugin/services/stripe-api.ts
|
|
6670
|
+
var StripeAPI = class {
|
|
6671
|
+
constructor(secretKey) {
|
|
6672
|
+
this.secretKey = secretKey;
|
|
6673
|
+
}
|
|
6674
|
+
baseUrl = "https://api.stripe.com/v1";
|
|
6675
|
+
/**
|
|
6676
|
+
* Verify a webhook signature
|
|
6677
|
+
* Implements Stripe's v1 signature scheme using Web Crypto API
|
|
6678
|
+
*/
|
|
6679
|
+
async verifyWebhookSignature(payload, sigHeader, secret) {
|
|
6680
|
+
const parts = sigHeader.split(",");
|
|
6681
|
+
const timestamp = parts.find((p) => p.startsWith("t="))?.split("=")[1];
|
|
6682
|
+
const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.substring(3));
|
|
6683
|
+
if (!timestamp || signatures.length === 0) return false;
|
|
6684
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
6685
|
+
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
|
|
6686
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
6687
|
+
const encoder = new TextEncoder();
|
|
6688
|
+
const key = await crypto.subtle.importKey(
|
|
6689
|
+
"raw",
|
|
6690
|
+
encoder.encode(secret),
|
|
6691
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
6692
|
+
false,
|
|
6693
|
+
["sign"]
|
|
6694
|
+
);
|
|
6695
|
+
const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload));
|
|
6696
|
+
const expectedSignature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6697
|
+
return signatures.some((sig) => timingSafeEqual(sig, expectedSignature));
|
|
6698
|
+
}
|
|
6699
|
+
/**
|
|
6700
|
+
* Create a Checkout Session
|
|
6701
|
+
*/
|
|
6702
|
+
async createCheckoutSession(params) {
|
|
6703
|
+
const body = new URLSearchParams();
|
|
6704
|
+
body.append("mode", "subscription");
|
|
6705
|
+
body.append("line_items[0][price]", params.priceId);
|
|
6706
|
+
body.append("line_items[0][quantity]", "1");
|
|
6707
|
+
body.append("success_url", params.successUrl);
|
|
6708
|
+
body.append("cancel_url", params.cancelUrl);
|
|
6709
|
+
if (params.customerId) {
|
|
6710
|
+
body.append("customer", params.customerId);
|
|
6711
|
+
} else if (params.customerEmail) {
|
|
6712
|
+
body.append("customer_email", params.customerEmail);
|
|
6713
|
+
}
|
|
6714
|
+
if (params.metadata) {
|
|
6715
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
6716
|
+
body.append(`metadata[${key}]`, value);
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
const response = await this.request("POST", "/checkout/sessions", body);
|
|
6720
|
+
return { id: response.id, url: response.url };
|
|
6721
|
+
}
|
|
6722
|
+
/**
|
|
6723
|
+
* Retrieve a Stripe subscription
|
|
6724
|
+
*/
|
|
6725
|
+
async getSubscription(subscriptionId) {
|
|
6726
|
+
return this.request("GET", `/subscriptions/${subscriptionId}`);
|
|
6727
|
+
}
|
|
6728
|
+
/**
|
|
6729
|
+
* Create a Stripe customer
|
|
6730
|
+
*/
|
|
6731
|
+
async createCustomer(params) {
|
|
6732
|
+
const body = new URLSearchParams();
|
|
6733
|
+
body.append("email", params.email);
|
|
6734
|
+
if (params.metadata) {
|
|
6735
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
6736
|
+
body.append(`metadata[${key}]`, value);
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
return this.request("POST", "/customers", body);
|
|
6740
|
+
}
|
|
6741
|
+
/**
|
|
6742
|
+
* Search for a customer by email
|
|
6743
|
+
*/
|
|
6744
|
+
async findCustomerByEmail(email) {
|
|
6745
|
+
const params = new URLSearchParams();
|
|
6746
|
+
params.append("query", `email:'${email}'`);
|
|
6747
|
+
params.append("limit", "1");
|
|
6748
|
+
const result = await this.request("GET", `/customers/search?${params.toString()}`);
|
|
6749
|
+
return result.data?.[0] || null;
|
|
6750
|
+
}
|
|
6751
|
+
async request(method, path, body) {
|
|
6752
|
+
const url = path.startsWith("http") ? path : `${this.baseUrl}${path}`;
|
|
6753
|
+
const response = await fetch(url, {
|
|
6754
|
+
method,
|
|
6755
|
+
headers: {
|
|
6756
|
+
"Authorization": `Bearer ${this.secretKey}`,
|
|
6757
|
+
...body ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
|
|
6758
|
+
},
|
|
6759
|
+
...body ? { body: body.toString() } : {}
|
|
6760
|
+
});
|
|
6761
|
+
const data = await response.json();
|
|
6762
|
+
if (!response.ok) {
|
|
6763
|
+
throw new Error(`Stripe API error: ${data.error?.message || response.statusText}`);
|
|
6764
|
+
}
|
|
6765
|
+
return data;
|
|
6766
|
+
}
|
|
6767
|
+
};
|
|
6768
|
+
function timingSafeEqual(a, b) {
|
|
6769
|
+
if (a.length !== b.length) return false;
|
|
6770
|
+
let result = 0;
|
|
6771
|
+
for (let i = 0; i < a.length; i++) {
|
|
6772
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6773
|
+
}
|
|
6774
|
+
return result === 0;
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
// src/plugins/core-plugins/stripe-plugin/routes/api.ts
|
|
6778
|
+
var apiRoutes3 = new Hono();
|
|
6779
|
+
async function getSettings3(db) {
|
|
6780
|
+
try {
|
|
6781
|
+
const pluginService = new PluginService(db);
|
|
6782
|
+
const plugin2 = await pluginService.getPlugin("stripe");
|
|
6783
|
+
if (plugin2?.settings) {
|
|
6784
|
+
const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
|
|
6785
|
+
return { ...DEFAULT_SETTINGS3, ...settings };
|
|
6786
|
+
}
|
|
6787
|
+
} catch {
|
|
6788
|
+
}
|
|
6789
|
+
return DEFAULT_SETTINGS3;
|
|
6790
|
+
}
|
|
6791
|
+
function mapStripeStatus(status) {
|
|
6792
|
+
const map = {
|
|
6793
|
+
active: "active",
|
|
6794
|
+
canceled: "canceled",
|
|
6795
|
+
past_due: "past_due",
|
|
6796
|
+
trialing: "trialing",
|
|
6797
|
+
unpaid: "unpaid",
|
|
6798
|
+
paused: "paused",
|
|
6799
|
+
incomplete: "incomplete",
|
|
6800
|
+
incomplete_expired: "incomplete_expired"
|
|
6801
|
+
};
|
|
6802
|
+
return map[status] || "incomplete";
|
|
6803
|
+
}
|
|
6804
|
+
apiRoutes3.post("/webhook", async (c) => {
|
|
6805
|
+
const db = c.env.DB;
|
|
6806
|
+
const settings = await getSettings3(db);
|
|
6807
|
+
if (!settings.stripeWebhookSecret) {
|
|
6808
|
+
return c.json({ error: "Webhook secret not configured" }, 500);
|
|
6809
|
+
}
|
|
6810
|
+
const rawBody = await c.req.text();
|
|
6811
|
+
const sigHeader = c.req.header("stripe-signature") || "";
|
|
6812
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
6813
|
+
const isValid = await stripeApi.verifyWebhookSignature(rawBody, sigHeader, settings.stripeWebhookSecret);
|
|
6814
|
+
if (!isValid) {
|
|
6815
|
+
return c.json({ error: "Invalid signature" }, 400);
|
|
6816
|
+
}
|
|
6817
|
+
const event = JSON.parse(rawBody);
|
|
6818
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6819
|
+
await subscriptionService.ensureTable();
|
|
6820
|
+
try {
|
|
6821
|
+
switch (event.type) {
|
|
6822
|
+
case "customer.subscription.created": {
|
|
6823
|
+
const sub = event.data.object;
|
|
6824
|
+
const userId = sub.metadata?.sonicjs_user_id || await subscriptionService.getUserIdByStripeCustomer(sub.customer) || "";
|
|
6825
|
+
await subscriptionService.create({
|
|
6826
|
+
userId,
|
|
6827
|
+
stripeCustomerId: sub.customer,
|
|
6828
|
+
stripeSubscriptionId: sub.id,
|
|
6829
|
+
stripePriceId: sub.items.data[0]?.price.id || "",
|
|
6830
|
+
status: mapStripeStatus(sub.status),
|
|
6831
|
+
currentPeriodStart: sub.current_period_start,
|
|
6832
|
+
currentPeriodEnd: sub.current_period_end,
|
|
6833
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
6834
|
+
});
|
|
6835
|
+
console.log(`[Stripe] Subscription created: ${sub.id}`);
|
|
6836
|
+
break;
|
|
6837
|
+
}
|
|
6838
|
+
case "customer.subscription.updated": {
|
|
6839
|
+
const sub = event.data.object;
|
|
6840
|
+
await subscriptionService.updateByStripeId(sub.id, {
|
|
6841
|
+
status: mapStripeStatus(sub.status),
|
|
6842
|
+
stripePriceId: sub.items.data[0]?.price.id || void 0,
|
|
6843
|
+
currentPeriodStart: sub.current_period_start,
|
|
6844
|
+
currentPeriodEnd: sub.current_period_end,
|
|
6845
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
6846
|
+
});
|
|
6847
|
+
console.log(`[Stripe] Subscription updated: ${sub.id} -> ${sub.status}`);
|
|
6848
|
+
break;
|
|
6849
|
+
}
|
|
6850
|
+
case "customer.subscription.deleted": {
|
|
6851
|
+
const sub = event.data.object;
|
|
6852
|
+
await subscriptionService.updateByStripeId(sub.id, {
|
|
6853
|
+
status: "canceled"
|
|
6854
|
+
});
|
|
6855
|
+
console.log(`[Stripe] Subscription deleted: ${sub.id}`);
|
|
6856
|
+
break;
|
|
6857
|
+
}
|
|
6858
|
+
case "checkout.session.completed": {
|
|
6859
|
+
const session = event.data.object;
|
|
6860
|
+
const userId = session.metadata?.sonicjs_user_id;
|
|
6861
|
+
if (userId && session.subscription) {
|
|
6862
|
+
const existing = await subscriptionService.getByStripeSubscriptionId(session.subscription);
|
|
6863
|
+
if (existing && !existing.userId) {
|
|
6864
|
+
await subscriptionService.updateByStripeId(session.subscription, {
|
|
6865
|
+
userId
|
|
6866
|
+
});
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
console.log(`[Stripe] Checkout completed: ${session.id}`);
|
|
6870
|
+
break;
|
|
6871
|
+
}
|
|
6872
|
+
case "invoice.payment_succeeded": {
|
|
6873
|
+
const invoice = event.data.object;
|
|
6874
|
+
if (invoice.subscription) {
|
|
6875
|
+
await subscriptionService.updateByStripeId(invoice.subscription, {
|
|
6876
|
+
status: "active"
|
|
6877
|
+
});
|
|
6878
|
+
}
|
|
6879
|
+
console.log(`[Stripe] Payment succeeded for invoice: ${invoice.id}`);
|
|
6880
|
+
break;
|
|
6881
|
+
}
|
|
6882
|
+
case "invoice.payment_failed": {
|
|
6883
|
+
const invoice = event.data.object;
|
|
6884
|
+
if (invoice.subscription) {
|
|
6885
|
+
await subscriptionService.updateByStripeId(invoice.subscription, {
|
|
6886
|
+
status: "past_due"
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
console.log(`[Stripe] Payment failed for invoice: ${invoice.id}`);
|
|
6890
|
+
break;
|
|
6891
|
+
}
|
|
6892
|
+
default:
|
|
6893
|
+
console.log(`[Stripe] Unhandled event type: ${event.type}`);
|
|
6894
|
+
}
|
|
6895
|
+
} catch (error) {
|
|
6896
|
+
console.error(`[Stripe] Error processing webhook event ${event.type}:`, error);
|
|
6897
|
+
return c.json({ error: "Webhook processing failed" }, 500);
|
|
6898
|
+
}
|
|
6899
|
+
return c.json({ received: true });
|
|
6900
|
+
});
|
|
6901
|
+
apiRoutes3.post("/create-checkout-session", requireAuth(), async (c) => {
|
|
6902
|
+
const db = c.env.DB;
|
|
6903
|
+
const user = c.get("user");
|
|
6904
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
6905
|
+
const settings = await getSettings3(db);
|
|
6906
|
+
if (!settings.stripeSecretKey) {
|
|
6907
|
+
return c.json({ error: "Stripe not configured" }, 500);
|
|
6908
|
+
}
|
|
6909
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6910
|
+
const priceId = body.priceId || settings.stripePriceId;
|
|
6911
|
+
if (!priceId) {
|
|
6912
|
+
return c.json({ error: "No price ID specified" }, 400);
|
|
6913
|
+
}
|
|
6914
|
+
const stripeApi = new StripeAPI(settings.stripeSecretKey);
|
|
6915
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6916
|
+
await subscriptionService.ensureTable();
|
|
6917
|
+
const existingSub = await subscriptionService.getByUserId(user.userId);
|
|
6918
|
+
let customerId = existingSub?.stripeCustomerId;
|
|
6919
|
+
if (!customerId) {
|
|
6920
|
+
const existing = await stripeApi.findCustomerByEmail(user.email);
|
|
6921
|
+
if (existing) {
|
|
6922
|
+
customerId = existing.id;
|
|
6923
|
+
} else {
|
|
6924
|
+
const customer = await stripeApi.createCustomer({
|
|
6925
|
+
email: user.email,
|
|
6926
|
+
metadata: { sonicjs_user_id: user.userId }
|
|
6927
|
+
});
|
|
6928
|
+
customerId = customer.id;
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
const origin = new URL(c.req.url).origin;
|
|
6932
|
+
const session = await stripeApi.createCheckoutSession({
|
|
6933
|
+
priceId,
|
|
6934
|
+
customerId,
|
|
6935
|
+
successUrl: `${origin}${settings.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
|
|
6936
|
+
cancelUrl: `${origin}${settings.cancelUrl}`,
|
|
6937
|
+
metadata: { sonicjs_user_id: user.userId }
|
|
6938
|
+
});
|
|
6939
|
+
return c.json({ sessionId: session.id, url: session.url });
|
|
6940
|
+
});
|
|
6941
|
+
apiRoutes3.get("/subscription", requireAuth(), async (c) => {
|
|
6942
|
+
const user = c.get("user");
|
|
6943
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
6944
|
+
const db = c.env.DB;
|
|
6945
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6946
|
+
await subscriptionService.ensureTable();
|
|
6947
|
+
const subscription = await subscriptionService.getByUserId(user.userId);
|
|
6948
|
+
if (!subscription) {
|
|
6949
|
+
return c.json({ subscription: null });
|
|
6950
|
+
}
|
|
6951
|
+
return c.json({ subscription });
|
|
6952
|
+
});
|
|
6953
|
+
apiRoutes3.get("/subscriptions", requireAuth(), async (c) => {
|
|
6954
|
+
const user = c.get("user");
|
|
6955
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
6956
|
+
const db = c.env.DB;
|
|
6957
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6958
|
+
await subscriptionService.ensureTable();
|
|
6959
|
+
const filters = {
|
|
6960
|
+
status: c.req.query("status"),
|
|
6961
|
+
page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
|
|
6962
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
|
|
6963
|
+
sortBy: c.req.query("sortBy") || "created_at",
|
|
6964
|
+
sortOrder: c.req.query("sortOrder") || "desc"
|
|
6965
|
+
};
|
|
6966
|
+
const result = await subscriptionService.list(filters);
|
|
6967
|
+
return c.json(result);
|
|
6968
|
+
});
|
|
6969
|
+
apiRoutes3.get("/stats", requireAuth(), async (c) => {
|
|
6970
|
+
const user = c.get("user");
|
|
6971
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
6972
|
+
const db = c.env.DB;
|
|
6973
|
+
const subscriptionService = new SubscriptionService(db);
|
|
6974
|
+
await subscriptionService.ensureTable();
|
|
6975
|
+
const stats = await subscriptionService.getStats();
|
|
6976
|
+
return c.json(stats);
|
|
6977
|
+
});
|
|
6978
|
+
|
|
6979
|
+
// src/plugins/core-plugins/stripe-plugin/index.ts
|
|
6980
|
+
function createStripePlugin() {
|
|
6981
|
+
const builder = PluginBuilder.create({
|
|
6982
|
+
name: "stripe",
|
|
6983
|
+
version: "1.0.0-beta.1",
|
|
6984
|
+
description: "Stripe subscription management with webhook handling, checkout sessions, and subscription gating"
|
|
6985
|
+
});
|
|
6986
|
+
builder.metadata({
|
|
6987
|
+
author: { name: "SonicJS Team" },
|
|
6988
|
+
license: "MIT"
|
|
6989
|
+
});
|
|
6990
|
+
builder.addRoute("/admin/plugins/stripe", adminRoutes3, {
|
|
6991
|
+
description: "Stripe subscriptions admin dashboard",
|
|
6992
|
+
requiresAuth: true,
|
|
6993
|
+
priority: 50
|
|
6994
|
+
});
|
|
6995
|
+
builder.addRoute("/api/stripe", apiRoutes3, {
|
|
6996
|
+
description: "Stripe API endpoints (webhook, checkout, subscription)",
|
|
6997
|
+
requiresAuth: false,
|
|
6998
|
+
// Webhook route handles its own auth via signature
|
|
6999
|
+
priority: 50
|
|
7000
|
+
});
|
|
7001
|
+
builder.addMenuItem("Stripe", "/admin/plugins/stripe", {
|
|
7002
|
+
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>`,
|
|
7003
|
+
order: 75
|
|
7004
|
+
});
|
|
7005
|
+
builder.lifecycle({
|
|
7006
|
+
install: async () => {
|
|
7007
|
+
console.log("[Stripe] Plugin installed");
|
|
7008
|
+
},
|
|
7009
|
+
activate: async () => {
|
|
7010
|
+
console.log("[Stripe] Plugin activated");
|
|
7011
|
+
},
|
|
7012
|
+
deactivate: async () => {
|
|
7013
|
+
console.log("[Stripe] Plugin deactivated");
|
|
7014
|
+
},
|
|
7015
|
+
uninstall: async () => {
|
|
7016
|
+
console.log("[Stripe] Plugin uninstalled");
|
|
7017
|
+
}
|
|
7018
|
+
});
|
|
7019
|
+
return builder.build();
|
|
7020
|
+
}
|
|
7021
|
+
var stripePlugin = createStripePlugin();
|
|
7022
|
+
|
|
6262
7023
|
// src/middleware/plugin-menu.ts
|
|
6263
7024
|
var MENU_PLUGINS = [
|
|
6264
7025
|
securityAuditPlugin
|
|
@@ -8244,6 +9005,11 @@ function createSonicJSApp(config = {}) {
|
|
|
8244
9005
|
app2.route("/admin", userRoutes);
|
|
8245
9006
|
app2.route("/auth", auth_default);
|
|
8246
9007
|
app2.route("/", test_cleanup_default);
|
|
9008
|
+
if (stripePlugin.routes && stripePlugin.routes.length > 0) {
|
|
9009
|
+
for (const route of stripePlugin.routes) {
|
|
9010
|
+
app2.route(route.path, route.handler);
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
8247
9013
|
if (emailPlugin.routes && emailPlugin.routes.length > 0) {
|
|
8248
9014
|
for (const route of emailPlugin.routes) {
|
|
8249
9015
|
app2.route(route.path, route.handler);
|