@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Stripe Pricing Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.toast { animation: slideIn 0.3s ease-out; }
|
|
10
|
+
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
11
|
+
.fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
12
|
+
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="bg-gray-100">
|
|
16
|
+
<div class="min-h-screen">
|
|
17
|
+
<div class="bg-white shadow">
|
|
18
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
19
|
+
<div class="flex justify-between items-center">
|
|
20
|
+
<div>
|
|
21
|
+
<h1 class="text-2xl font-bold text-gray-900">Stripe Pricing</h1>
|
|
22
|
+
<p class="text-sm text-gray-600 mt-1">Manage Stripe prices and products</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex items-center gap-4">
|
|
25
|
+
<span id="stripe-status" class="text-sm"></span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div id="stripe-disabled" class="hidden max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
32
|
+
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
|
33
|
+
<h2 class="text-lg font-semibold text-yellow-800 mb-2">Stripe Not Configured</h2>
|
|
34
|
+
<p class="text-yellow-700 mb-4">To use this feature, set the <code class="bg-yellow-100 px-1 rounded">STRIPE_SECRET_KEY</code> environment variable.</p>
|
|
35
|
+
<p class="text-sm text-yellow-600">Example: <code class="bg-yellow-100 px-1 rounded">STRIPE_SECRET_KEY=sk_test_...</code></p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div id="stripe-enabled" class="hidden max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
40
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
41
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
42
|
+
<p class="text-sm text-gray-500">Total Catalog Items</p>
|
|
43
|
+
<p id="stat-total" class="text-2xl font-bold text-gray-900">-</p>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
46
|
+
<p class="text-sm text-gray-500">Active Items</p>
|
|
47
|
+
<p id="stat-active" class="text-2xl font-bold text-green-600">-</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
50
|
+
<p class="text-sm text-gray-500">Env: PRICE_ID_CREATOR</p>
|
|
51
|
+
<p id="stat-env-creator" class="text-sm font-medium text-gray-700">-</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
54
|
+
<p class="text-sm text-gray-500">Env: PRICE_ID_PRO</p>
|
|
55
|
+
<p id="stat-env-pro" class="text-sm font-medium text-gray-700">-</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
60
|
+
<div class="lg:col-span-1 space-y-6">
|
|
61
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
62
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Create New Price</h2>
|
|
63
|
+
<div class="space-y-3">
|
|
64
|
+
<div>
|
|
65
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Product Name *</label>
|
|
66
|
+
<input id="create-product-name" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="Pro Plan">
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
70
|
+
<input id="create-description" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="Access to all features">
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Plan Key *</label>
|
|
74
|
+
<input id="create-plan-key" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="pro_monthly">
|
|
75
|
+
</div>
|
|
76
|
+
<div>
|
|
77
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Display Name *</label>
|
|
78
|
+
<input id="create-display-name" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="Pro (Monthly)">
|
|
79
|
+
</div>
|
|
80
|
+
<div class="grid grid-cols-2 gap-3">
|
|
81
|
+
<div>
|
|
82
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Type *</label>
|
|
83
|
+
<select id="create-billing-type" class="w-full border rounded px-3 py-2 text-sm">
|
|
84
|
+
<option value="subscription">Subscription</option>
|
|
85
|
+
<option value="one_time">One-Time</option>
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Currency</label>
|
|
90
|
+
<select id="create-currency" class="w-full border rounded px-3 py-2 text-sm">
|
|
91
|
+
<option value="usd">USD</option>
|
|
92
|
+
<option value="eur">EUR</option>
|
|
93
|
+
<option value="gbp">GBP</option>
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="grid grid-cols-2 gap-3">
|
|
98
|
+
<div>
|
|
99
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Amount (cents) *</label>
|
|
100
|
+
<input id="create-unit-amount" type="number" min="0" class="w-full border rounded px-3 py-2 text-sm" placeholder="1999">
|
|
101
|
+
</div>
|
|
102
|
+
<div id="interval-container">
|
|
103
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Interval</label>
|
|
104
|
+
<select id="create-interval" class="w-full border rounded px-3 py-2 text-sm">
|
|
105
|
+
<option value="month">Monthly</option>
|
|
106
|
+
<option value="year">Yearly</option>
|
|
107
|
+
<option value="week">Weekly</option>
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<button id="btn-create" class="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 text-sm">Create in Stripe + Catalog</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
116
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Import Existing Price</h2>
|
|
117
|
+
<p class="text-xs text-gray-500 mb-3">Import an existing Stripe price by ID and assign a plan key.</p>
|
|
118
|
+
<div class="space-y-3">
|
|
119
|
+
<div>
|
|
120
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Stripe Price ID *</label>
|
|
121
|
+
<input id="import-price-id" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="price_...">
|
|
122
|
+
</div>
|
|
123
|
+
<div>
|
|
124
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Plan Key *</label>
|
|
125
|
+
<input id="import-plan-key" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="pro">
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Display Name</label>
|
|
129
|
+
<input id="import-display-name" type="text" class="w-full border rounded px-3 py-2 text-sm" placeholder="(auto from Stripe)">
|
|
130
|
+
</div>
|
|
131
|
+
<button id="btn-import" class="w-full bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 text-sm">Import Price</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="lg:col-span-2">
|
|
137
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
138
|
+
<div class="flex items-center justify-between mb-4">
|
|
139
|
+
<div>
|
|
140
|
+
<h2 class="text-lg font-semibold text-gray-900">Catalog</h2>
|
|
141
|
+
<p id="catalog-subtitle" class="text-sm text-gray-600">-</p>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="flex gap-2">
|
|
144
|
+
<button id="btn-show-stripe" class="bg-purple-100 text-purple-800 px-3 py-2 rounded hover:bg-purple-200 text-sm">Browse Stripe</button>
|
|
145
|
+
<button id="btn-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200 text-sm">Refresh</button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="flex gap-2 mb-4">
|
|
150
|
+
<select id="filter-active" class="border rounded px-3 py-2 text-sm">
|
|
151
|
+
<option value="">All Status</option>
|
|
152
|
+
<option value="true">Active</option>
|
|
153
|
+
<option value="false">Inactive</option>
|
|
154
|
+
</select>
|
|
155
|
+
<select id="filter-type" class="border rounded px-3 py-2 text-sm">
|
|
156
|
+
<option value="">All Types</option>
|
|
157
|
+
<option value="subscription">Subscription</option>
|
|
158
|
+
<option value="one_time">One-Time</option>
|
|
159
|
+
</select>
|
|
160
|
+
<button id="btn-apply" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 text-sm">Apply</button>
|
|
161
|
+
<div class="flex-1"></div>
|
|
162
|
+
<button id="btn-prev" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200 text-sm" disabled>Prev</button>
|
|
163
|
+
<button id="btn-next" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200 text-sm" disabled>Next</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="overflow-x-auto">
|
|
167
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
168
|
+
<thead class="bg-gray-50">
|
|
169
|
+
<tr>
|
|
170
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Plan Key</th>
|
|
171
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Display</th>
|
|
172
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
|
173
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Price</th>
|
|
174
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
175
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
176
|
+
</tr>
|
|
177
|
+
</thead>
|
|
178
|
+
<tbody id="catalog-tbody" class="bg-white divide-y divide-gray-200"></tbody>
|
|
179
|
+
</table>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div id="modal-stripe" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
|
|
188
|
+
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-3xl mx-4 max-h-[80vh] overflow-y-auto">
|
|
189
|
+
<div class="flex justify-between items-center mb-4">
|
|
190
|
+
<h3 class="text-lg font-semibold text-gray-900">Stripe Prices</h3>
|
|
191
|
+
<button id="btn-close-stripe" class="text-gray-400 hover:text-gray-600">×</button>
|
|
192
|
+
</div>
|
|
193
|
+
<p class="text-sm text-gray-600 mb-4">Prices from your Stripe account. Click "Import" to add to catalog.</p>
|
|
194
|
+
<div class="overflow-x-auto">
|
|
195
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
196
|
+
<thead class="bg-gray-50">
|
|
197
|
+
<tr>
|
|
198
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
|
|
199
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Price ID</th>
|
|
200
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
|
|
201
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
|
202
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Mapped</th>
|
|
203
|
+
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
|
|
204
|
+
</tr>
|
|
205
|
+
</thead>
|
|
206
|
+
<tbody id="stripe-prices-tbody" class="bg-white divide-y divide-gray-200"></tbody>
|
|
207
|
+
</table>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="flex justify-end mt-4">
|
|
210
|
+
<button id="btn-load-more-stripe" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200 text-sm hidden">Load More</button>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
216
|
+
|
|
217
|
+
<script>
|
|
218
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
219
|
+
|
|
220
|
+
function showToast(message, type = 'success') {
|
|
221
|
+
const container = document.getElementById('toast-container');
|
|
222
|
+
const toast = document.createElement('div');
|
|
223
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${type === 'success' ? 'bg-green-500' : 'bg-red-500'}`;
|
|
224
|
+
toast.textContent = message;
|
|
225
|
+
container.appendChild(toast);
|
|
226
|
+
setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => toast.remove(), 300); }, 3000);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function escapeHtml(str) {
|
|
230
|
+
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatAmount(cents, currency) {
|
|
234
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'usd' }).format(cents / 100);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function qs(obj) {
|
|
238
|
+
const params = new URLSearchParams();
|
|
239
|
+
Object.entries(obj).forEach(([k, v]) => { if (v !== undefined && v !== null && String(v).trim()) params.set(k, String(v).trim()); });
|
|
240
|
+
const out = params.toString();
|
|
241
|
+
return out ? `?${out}` : '';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const state = { offset: 0, limit: 20, total: 0, stripeConfigured: false };
|
|
245
|
+
|
|
246
|
+
async function checkStatus() {
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/status`);
|
|
249
|
+
const data = await res.json();
|
|
250
|
+
state.stripeConfigured = data.configured;
|
|
251
|
+
|
|
252
|
+
if (data.configured) {
|
|
253
|
+
document.getElementById('stripe-enabled').classList.remove('hidden');
|
|
254
|
+
document.getElementById('stripe-disabled').classList.add('hidden');
|
|
255
|
+
document.getElementById('stripe-status').innerHTML = '<span class="text-green-600">● Connected</span>';
|
|
256
|
+
document.getElementById('stat-total').textContent = data.catalogCount ?? '-';
|
|
257
|
+
document.getElementById('stat-active').textContent = data.activeCount ?? '-';
|
|
258
|
+
document.getElementById('stat-env-creator').textContent = data.envPriceIdCreator;
|
|
259
|
+
document.getElementById('stat-env-pro').textContent = data.envPriceIdPro;
|
|
260
|
+
loadCatalog();
|
|
261
|
+
} else {
|
|
262
|
+
document.getElementById('stripe-enabled').classList.add('hidden');
|
|
263
|
+
document.getElementById('stripe-disabled').classList.remove('hidden');
|
|
264
|
+
document.getElementById('stripe-status').innerHTML = '<span class="text-yellow-600">● Not Configured</span>';
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.error('Status check failed:', e);
|
|
268
|
+
document.getElementById('stripe-status').innerHTML = '<span class="text-red-600">● Error</span>';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function loadCatalog() {
|
|
273
|
+
const active = document.getElementById('filter-active')?.value;
|
|
274
|
+
const billingType = document.getElementById('filter-type')?.value;
|
|
275
|
+
|
|
276
|
+
const subtitle = document.getElementById('catalog-subtitle');
|
|
277
|
+
const tbody = document.getElementById('catalog-tbody');
|
|
278
|
+
if (subtitle) subtitle.textContent = 'Loading...';
|
|
279
|
+
if (tbody) tbody.innerHTML = '';
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const url = `${API_BASE}/api/admin/stripe/catalog${qs({ active, billingType, limit: state.limit, offset: state.offset })}`;
|
|
283
|
+
const res = await fetch(url);
|
|
284
|
+
const data = await res.json();
|
|
285
|
+
|
|
286
|
+
if (!res.ok) { showToast(data?.error || 'Failed to load catalog', 'error'); return; }
|
|
287
|
+
|
|
288
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
289
|
+
const pagination = data?.pagination || {};
|
|
290
|
+
state.total = pagination.total ?? 0;
|
|
291
|
+
state.offset = pagination.offset ?? state.offset;
|
|
292
|
+
|
|
293
|
+
if (subtitle) {
|
|
294
|
+
const from = state.total === 0 ? 0 : state.offset + 1;
|
|
295
|
+
const to = Math.min(state.offset + items.length, state.total);
|
|
296
|
+
subtitle.textContent = `${from}-${to} of ${state.total}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
document.getElementById('btn-prev').disabled = state.offset <= 0;
|
|
300
|
+
document.getElementById('btn-next').disabled = state.offset + state.limit >= state.total;
|
|
301
|
+
|
|
302
|
+
if (tbody) {
|
|
303
|
+
if (items.length === 0) {
|
|
304
|
+
tbody.innerHTML = '<tr><td class="px-3 py-4 text-sm text-gray-600" colspan="6">No catalog items. Create one or import from Stripe.</td></tr>';
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
tbody.innerHTML = items.map(item => {
|
|
309
|
+
const priceStr = formatAmount(item.unitAmount, item.currency) + (item.interval ? `/${item.interval}` : '');
|
|
310
|
+
const statusClass = item.active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600';
|
|
311
|
+
return `
|
|
312
|
+
<tr>
|
|
313
|
+
<td class="px-3 py-2 text-sm">
|
|
314
|
+
<div class="font-medium text-gray-900">${escapeHtml(item.planKey)}</div>
|
|
315
|
+
<div class="text-xs text-gray-400 truncate max-w-[150px]" title="${escapeHtml(item.stripePriceId)}">${escapeHtml(item.stripePriceId)}</div>
|
|
316
|
+
</td>
|
|
317
|
+
<td class="px-3 py-2 text-sm text-gray-700">${escapeHtml(item.displayName)}</td>
|
|
318
|
+
<td class="px-3 py-2 text-sm text-gray-700">${escapeHtml(item.billingType)}</td>
|
|
319
|
+
<td class="px-3 py-2 text-sm text-gray-700">${priceStr}</td>
|
|
320
|
+
<td class="px-3 py-2"><span class="px-2 py-1 text-xs rounded ${statusClass}">${item.active ? 'Active' : 'Inactive'}</span></td>
|
|
321
|
+
<td class="px-3 py-2 text-sm whitespace-nowrap">
|
|
322
|
+
<button class="text-blue-600 hover:text-blue-800 mr-1" onclick="copyText('${escapeHtml(item.stripePriceId)}')">Copy</button>
|
|
323
|
+
${item.active
|
|
324
|
+
? `<button class="text-yellow-600 hover:text-yellow-800 mr-1" data-deactivate="${escapeHtml(item._id)}">Deact</button>`
|
|
325
|
+
: `<button class="text-green-600 hover:text-green-800 mr-1" data-activate="${escapeHtml(item._id)}">Act</button>`
|
|
326
|
+
}
|
|
327
|
+
<button class="text-red-600 hover:text-red-800" data-delete="${escapeHtml(item._id)}">Del</button>
|
|
328
|
+
</td>
|
|
329
|
+
</tr>
|
|
330
|
+
`;
|
|
331
|
+
}).join('');
|
|
332
|
+
|
|
333
|
+
tbody.querySelectorAll('[data-deactivate]').forEach(btn => {
|
|
334
|
+
btn.addEventListener('click', async () => {
|
|
335
|
+
if (!confirm('Deactivate this catalog item?')) return;
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/catalog/${btn.dataset.deactivate}/deactivate`, { method: 'POST' });
|
|
338
|
+
if (!res.ok) { const d = await res.json(); showToast(d?.error || 'Failed', 'error'); return; }
|
|
339
|
+
showToast('Deactivated', 'success');
|
|
340
|
+
checkStatus();
|
|
341
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
tbody.querySelectorAll('[data-activate]').forEach(btn => {
|
|
346
|
+
btn.addEventListener('click', async () => {
|
|
347
|
+
try {
|
|
348
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/catalog/${btn.dataset.activate}/activate`, { method: 'POST' });
|
|
349
|
+
if (!res.ok) { const d = await res.json(); showToast(d?.error || 'Failed', 'error'); return; }
|
|
350
|
+
showToast('Activated', 'success');
|
|
351
|
+
checkStatus();
|
|
352
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
tbody.querySelectorAll('[data-delete]').forEach(btn => {
|
|
357
|
+
btn.addEventListener('click', async () => {
|
|
358
|
+
if (!confirm('Delete this catalog item? (Does not delete from Stripe)')) return;
|
|
359
|
+
try {
|
|
360
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/catalog/${btn.dataset.delete}`, { method: 'DELETE' });
|
|
361
|
+
if (!res.ok) { const d = await res.json(); showToast(d?.error || 'Failed', 'error'); return; }
|
|
362
|
+
showToast('Deleted', 'success');
|
|
363
|
+
checkStatus();
|
|
364
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
showToast(e.message || 'Failed to load catalog', 'error');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function copyText(text) {
|
|
374
|
+
navigator.clipboard.writeText(text).then(() => showToast('Copied!', 'success')).catch(() => showToast('Copy failed', 'error'));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function createPrice() {
|
|
378
|
+
const productName = document.getElementById('create-product-name').value.trim();
|
|
379
|
+
const productDescription = document.getElementById('create-description').value.trim();
|
|
380
|
+
const planKey = document.getElementById('create-plan-key').value.trim();
|
|
381
|
+
const displayName = document.getElementById('create-display-name').value.trim();
|
|
382
|
+
const billingType = document.getElementById('create-billing-type').value;
|
|
383
|
+
const currency = document.getElementById('create-currency').value;
|
|
384
|
+
const unitAmount = parseInt(document.getElementById('create-unit-amount').value, 10);
|
|
385
|
+
const interval = document.getElementById('create-interval').value;
|
|
386
|
+
|
|
387
|
+
if (!productName || !planKey || !displayName || !unitAmount) {
|
|
388
|
+
showToast('Fill required fields', 'error'); return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const body = { productName, productDescription, planKey, displayName, billingType, currency, unitAmount };
|
|
393
|
+
if (billingType === 'subscription') body.interval = interval;
|
|
394
|
+
|
|
395
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/catalog/upsert`, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
398
|
+
body: JSON.stringify(body)
|
|
399
|
+
});
|
|
400
|
+
const data = await res.json();
|
|
401
|
+
if (!res.ok) { showToast(data?.error || 'Failed', 'error'); return; }
|
|
402
|
+
showToast('Created: ' + data.stripePrice?.id, 'success');
|
|
403
|
+
document.getElementById('create-product-name').value = '';
|
|
404
|
+
document.getElementById('create-description').value = '';
|
|
405
|
+
document.getElementById('create-plan-key').value = '';
|
|
406
|
+
document.getElementById('create-display-name').value = '';
|
|
407
|
+
document.getElementById('create-unit-amount').value = '';
|
|
408
|
+
checkStatus();
|
|
409
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function importPrice() {
|
|
413
|
+
const stripePriceId = document.getElementById('import-price-id').value.trim();
|
|
414
|
+
const planKey = document.getElementById('import-plan-key').value.trim();
|
|
415
|
+
const displayName = document.getElementById('import-display-name').value.trim();
|
|
416
|
+
|
|
417
|
+
if (!stripePriceId || !planKey) {
|
|
418
|
+
showToast('Price ID and Plan Key required', 'error'); return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/catalog/import`, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
425
|
+
body: JSON.stringify({ stripePriceId, planKey, displayName: displayName || undefined })
|
|
426
|
+
});
|
|
427
|
+
const data = await res.json();
|
|
428
|
+
if (!res.ok) { showToast(data?.error || 'Failed', 'error'); return; }
|
|
429
|
+
showToast('Imported!', 'success');
|
|
430
|
+
document.getElementById('import-price-id').value = '';
|
|
431
|
+
document.getElementById('import-plan-key').value = '';
|
|
432
|
+
document.getElementById('import-display-name').value = '';
|
|
433
|
+
checkStatus();
|
|
434
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let stripePricesLastId = null;
|
|
438
|
+
let stripePricesHasMore = false;
|
|
439
|
+
|
|
440
|
+
async function loadStripePrices(append = false) {
|
|
441
|
+
const tbody = document.getElementById('stripe-prices-tbody');
|
|
442
|
+
if (!append) {
|
|
443
|
+
tbody.innerHTML = '<tr><td colspan="6" class="px-3 py-4 text-sm text-gray-600">Loading...</td></tr>';
|
|
444
|
+
stripePricesLastId = null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const params = { limit: 20 };
|
|
449
|
+
if (stripePricesLastId) params.starting_after = stripePricesLastId;
|
|
450
|
+
|
|
451
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/prices${qs(params)}`);
|
|
452
|
+
const data = await res.json();
|
|
453
|
+
if (!res.ok) { showToast(data?.error || 'Failed', 'error'); return; }
|
|
454
|
+
|
|
455
|
+
const prices = Array.isArray(data?.prices) ? data.prices : [];
|
|
456
|
+
stripePricesHasMore = data.hasMore;
|
|
457
|
+
|
|
458
|
+
if (prices.length > 0) {
|
|
459
|
+
stripePricesLastId = prices[prices.length - 1].id;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
document.getElementById('btn-load-more-stripe').classList.toggle('hidden', !stripePricesHasMore);
|
|
463
|
+
|
|
464
|
+
const rows = prices.map(p => {
|
|
465
|
+
const productName = typeof p.product === 'object' ? p.product.name : p.product;
|
|
466
|
+
const priceStr = formatAmount(p.unit_amount, p.currency) + (p.recurring ? `/${p.recurring.interval}` : ' one-time');
|
|
467
|
+
const mapped = p._isMapped ? '<span class="text-green-600">Yes</span>' : '<span class="text-gray-400">No</span>';
|
|
468
|
+
return `
|
|
469
|
+
<tr>
|
|
470
|
+
<td class="px-3 py-2 text-sm text-gray-900">${escapeHtml(productName)}</td>
|
|
471
|
+
<td class="px-3 py-2 text-sm text-gray-700 truncate max-w-[150px]" title="${escapeHtml(p.id)}">${escapeHtml(p.id)}</td>
|
|
472
|
+
<td class="px-3 py-2 text-sm text-gray-700">${priceStr}</td>
|
|
473
|
+
<td class="px-3 py-2 text-sm text-gray-700">${p.recurring ? 'Subscription' : 'One-Time'}</td>
|
|
474
|
+
<td class="px-3 py-2 text-sm">${mapped}</td>
|
|
475
|
+
<td class="px-3 py-2 text-sm">
|
|
476
|
+
${p._isMapped ? '-' : `<button class="text-green-600 hover:text-green-800" data-import-stripe="${escapeHtml(p.id)}">Import</button>`}
|
|
477
|
+
</td>
|
|
478
|
+
</tr>
|
|
479
|
+
`;
|
|
480
|
+
}).join('');
|
|
481
|
+
|
|
482
|
+
if (append) {
|
|
483
|
+
tbody.innerHTML += rows;
|
|
484
|
+
} else {
|
|
485
|
+
tbody.innerHTML = rows || '<tr><td colspan="6" class="px-3 py-4 text-sm text-gray-600">No prices found.</td></tr>';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
tbody.querySelectorAll('[data-import-stripe]').forEach(btn => {
|
|
489
|
+
btn.addEventListener('click', () => {
|
|
490
|
+
document.getElementById('import-price-id').value = btn.dataset.importStripe;
|
|
491
|
+
document.getElementById('modal-stripe').classList.add('hidden');
|
|
492
|
+
document.getElementById('import-plan-key').focus();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function toggleIntervalVisibility() {
|
|
499
|
+
const billingType = document.getElementById('create-billing-type').value;
|
|
500
|
+
document.getElementById('interval-container').style.display = billingType === 'subscription' ? 'block' : 'none';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function bindEvents() {
|
|
504
|
+
document.getElementById('btn-refresh').onclick = () => checkStatus();
|
|
505
|
+
document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadCatalog(); };
|
|
506
|
+
document.getElementById('btn-prev').onclick = () => { state.offset = Math.max(0, state.offset - state.limit); loadCatalog(); };
|
|
507
|
+
document.getElementById('btn-next').onclick = () => { state.offset += state.limit; loadCatalog(); };
|
|
508
|
+
document.getElementById('btn-create').onclick = createPrice;
|
|
509
|
+
document.getElementById('btn-import').onclick = importPrice;
|
|
510
|
+
document.getElementById('create-billing-type').onchange = toggleIntervalVisibility;
|
|
511
|
+
document.getElementById('btn-show-stripe').onclick = () => {
|
|
512
|
+
document.getElementById('modal-stripe').classList.remove('hidden');
|
|
513
|
+
loadStripePrices();
|
|
514
|
+
};
|
|
515
|
+
document.getElementById('btn-close-stripe').onclick = () => document.getElementById('modal-stripe').classList.add('hidden');
|
|
516
|
+
document.getElementById('btn-load-more-stripe').onclick = () => loadStripePrices(true);
|
|
517
|
+
|
|
518
|
+
const runEnvSyncBtn = document.getElementById('btn-env-sync-run');
|
|
519
|
+
|
|
520
|
+
if (runEnvSyncBtn) {
|
|
521
|
+
runEnvSyncBtn.onclick = async () => {
|
|
522
|
+
try {
|
|
523
|
+
const res = await fetch(`${API_BASE}/api/admin/stripe/env/sync`, {
|
|
524
|
+
method: 'POST'
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const data = await res.json().catch(() => ({}));
|
|
528
|
+
|
|
529
|
+
if (!res.ok) {
|
|
530
|
+
showToast(data.error || 'Failed to sync env', 'error');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const appliedCount = Array.isArray(data.applied) ? data.applied.length : 0;
|
|
535
|
+
const totalActive = typeof data.totalActive === 'number' ? data.totalActive : null;
|
|
536
|
+
const suffix = totalActive === null ? '' : ` (active: ${totalActive})`;
|
|
537
|
+
showToast(`Synced env for ${appliedCount} planKey(s)${suffix}.`, 'success');
|
|
538
|
+
} catch (e) {
|
|
539
|
+
showToast(e.message || 'Failed to sync env', 'error');
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
toggleIntervalVisibility();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
bindEvents();
|
|
547
|
+
checkStatus();
|
|
548
|
+
</script>
|
|
549
|
+
<script>
|
|
550
|
+
window.addEventListener("keydown", (e) => {
|
|
551
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
</script>
|
|
557
|
+
</body>
|
|
558
|
+
</html>
|