@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,223 @@
|
|
|
1
|
+
const User = require("../models/User");
|
|
2
|
+
const StripeWebhookEvent = require("../models/StripeWebhookEvent");
|
|
3
|
+
const asyncHandler = require("../utils/asyncHandler");
|
|
4
|
+
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
|
|
5
|
+
const stripeService = require("../services/stripe.service");
|
|
6
|
+
|
|
7
|
+
// Create Stripe Checkout Session
|
|
8
|
+
const createCheckoutSession = asyncHandler(async (req, res) => {
|
|
9
|
+
const { priceId, billingMode } = req.body;
|
|
10
|
+
const userId = req.user._id;
|
|
11
|
+
|
|
12
|
+
if (!priceId) {
|
|
13
|
+
return res.status(400).json({ error: "priceId is required" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get or create Stripe customer
|
|
17
|
+
let customerId = req.user.stripeCustomerId;
|
|
18
|
+
if (!customerId) {
|
|
19
|
+
const customer = await stripe.customers.create({
|
|
20
|
+
email: req.user.email,
|
|
21
|
+
metadata: { userId: userId.toString() },
|
|
22
|
+
});
|
|
23
|
+
customerId = customer.id;
|
|
24
|
+
req.user.stripeCustomerId = customerId;
|
|
25
|
+
await req.user.save();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const mode = billingMode === "payment" ? "payment" : "subscription";
|
|
29
|
+
|
|
30
|
+
const session = await stripe.checkout.sessions.create({
|
|
31
|
+
customer: customerId,
|
|
32
|
+
mode,
|
|
33
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
34
|
+
success_url: `${process.env.PUBLIC_URL || "http://localhost:3000"}/?checkout=success`,
|
|
35
|
+
cancel_url: `${process.env.PUBLIC_URL || "http://localhost:3000"}/?checkout=cancelled`,
|
|
36
|
+
metadata: {
|
|
37
|
+
userId: userId.toString(),
|
|
38
|
+
billingMode: mode,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
res.json({ sessionId: session.id, url: session.url });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create Stripe Customer Portal Session
|
|
46
|
+
const createPortalSession = asyncHandler(async (req, res) => {
|
|
47
|
+
const customerId = req.user.stripeCustomerId;
|
|
48
|
+
|
|
49
|
+
if (!customerId) {
|
|
50
|
+
return res.status(400).json({ error: "No Stripe customer found" });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
54
|
+
customer: customerId,
|
|
55
|
+
return_url: `${process.env.PUBLIC_URL || "http://localhost:3000"}${process.env.BILLING_RETURN_URL_RELATIVE || "/settings/billing"}`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
res.json({ url: session.url });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Stripe Webhook Handler
|
|
62
|
+
const handleWebhook = async (req, res) => {
|
|
63
|
+
const sig = req.headers["stripe-signature"];
|
|
64
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
65
|
+
|
|
66
|
+
let event;
|
|
67
|
+
try {
|
|
68
|
+
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("Webhook signature verification failed:", err.message);
|
|
71
|
+
return res.status(400).send(`Webhook Error: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let webhookEventDoc;
|
|
75
|
+
try {
|
|
76
|
+
// Check if webhook event already exists
|
|
77
|
+
const existingEvent = await StripeWebhookEvent.findOne({ stripeEventId: event.id });
|
|
78
|
+
if (existingEvent) {
|
|
79
|
+
console.log(`Webhook event ${event.id} already processed with status: ${existingEvent.status}`);
|
|
80
|
+
return res.json({ received: true, status: 'duplicate' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Persist webhook event to database
|
|
84
|
+
webhookEventDoc = new StripeWebhookEvent({
|
|
85
|
+
stripeEventId: event.id,
|
|
86
|
+
eventType: event.type,
|
|
87
|
+
data: event.data.object,
|
|
88
|
+
previousAttributes: event.data.previous_attributes,
|
|
89
|
+
api_version: event.api_version,
|
|
90
|
+
request: event.request,
|
|
91
|
+
status: "received",
|
|
92
|
+
});
|
|
93
|
+
await webhookEventDoc.save();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error("Error saving webhook event:", err);
|
|
96
|
+
// If it's a duplicate key error, the event was already processed
|
|
97
|
+
if (err.code === 11000) {
|
|
98
|
+
return res.json({ received: true, status: 'duplicate' });
|
|
99
|
+
}
|
|
100
|
+
// Continue processing for other errors
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
switch (event.type) {
|
|
105
|
+
case "checkout.session.completed":
|
|
106
|
+
await stripeService.handleCheckoutSessionCompleted(event.data.object);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case "customer.subscription.created":
|
|
110
|
+
await stripeService.handleSubscriptionCreated(event.data.object);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "customer.subscription.updated":
|
|
114
|
+
await stripeService.handleSubscriptionUpdated(
|
|
115
|
+
event.data.object,
|
|
116
|
+
event.data.previous_attributes
|
|
117
|
+
);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "customer.subscription.deleted":
|
|
121
|
+
await stripeService.handleSubscriptionDeleted(event.data.object);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "invoice.payment_succeeded":
|
|
125
|
+
await stripeService.handleInvoicePaymentSucceeded(event.data.object);
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case "invoice.payment_failed":
|
|
129
|
+
await stripeService.handleInvoicePaymentFailed(event.data.object);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Update webhook event status to processed
|
|
137
|
+
if (webhookEventDoc) {
|
|
138
|
+
webhookEventDoc.status = "processed";
|
|
139
|
+
webhookEventDoc.processedAt = new Date();
|
|
140
|
+
await webhookEventDoc.save();
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("Error processing webhook:", err);
|
|
144
|
+
|
|
145
|
+
// Update webhook event status to failed
|
|
146
|
+
if (webhookEventDoc) {
|
|
147
|
+
webhookEventDoc.status = "failed";
|
|
148
|
+
webhookEventDoc.retryCount = (webhookEventDoc.retryCount || 0) + 1;
|
|
149
|
+
webhookEventDoc.processingErrors.push({
|
|
150
|
+
message: err.message,
|
|
151
|
+
timestamp: new Date()
|
|
152
|
+
});
|
|
153
|
+
await webhookEventDoc.save();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return res.status(500).json({ error: "Webhook processing failed" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
res.json({ received: true });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Reconcile subscription for user
|
|
163
|
+
const reconcileSubscription = asyncHandler(async (req, res) => {
|
|
164
|
+
const user = req.user;
|
|
165
|
+
|
|
166
|
+
if (!user.stripeCustomerId) {
|
|
167
|
+
return res.json({ status: "success", message: "No Stripe customer found" });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fetch latest subscription from Stripe
|
|
171
|
+
const subscriptions = await stripe.subscriptions.list({
|
|
172
|
+
customer: user.stripeCustomerId,
|
|
173
|
+
limit: 1,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (subscriptions.data.length > 0) {
|
|
177
|
+
const subscription = subscriptions.data[0];
|
|
178
|
+
user.stripeSubscriptionId = subscription.id;
|
|
179
|
+
// Map Stripe subscription status to our schema
|
|
180
|
+
const statusMapping = {
|
|
181
|
+
'active': 'active',
|
|
182
|
+
'past_due': 'past_due',
|
|
183
|
+
'unpaid': 'unpaid',
|
|
184
|
+
'canceled': 'cancelled',
|
|
185
|
+
'incomplete': 'incomplete',
|
|
186
|
+
'incomplete_expired': 'incomplete_expired',
|
|
187
|
+
'trialing': 'trialing'
|
|
188
|
+
};
|
|
189
|
+
user.subscriptionStatus = statusMapping[subscription.status] || subscription.status;
|
|
190
|
+
await user.save();
|
|
191
|
+
} else {
|
|
192
|
+
// No active subscription found. Check for successful one-off (payment) checkouts
|
|
193
|
+
const sessions = await stripe.checkout.sessions.list({
|
|
194
|
+
customer: user.stripeCustomerId,
|
|
195
|
+
limit: 10,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const lifetimeSession = sessions.data.find((session) => {
|
|
199
|
+
const mode = session.metadata?.billingMode || session.mode;
|
|
200
|
+
return (
|
|
201
|
+
mode === "payment" &&
|
|
202
|
+
session.payment_status === "paid"
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (lifetimeSession) {
|
|
207
|
+
user.subscriptionStatus = "active";
|
|
208
|
+
} else {
|
|
209
|
+
user.subscriptionStatus = "none";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await user.save();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
res.json({ status: "success", subscriptionStatus: user.subscriptionStatus });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
createCheckoutSession,
|
|
220
|
+
createPortalSession,
|
|
221
|
+
handleWebhook,
|
|
222
|
+
reconcileSubscription,
|
|
223
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const {
|
|
2
|
+
evaluateAllForRequest,
|
|
3
|
+
flagsArrayToMap,
|
|
4
|
+
} = require('../services/featureFlags.service');
|
|
5
|
+
|
|
6
|
+
exports.getEvaluatedFlags = async (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
const userId = req.user?._id;
|
|
9
|
+
const orgId = req.query.orgId || req.headers['x-org-id'] || null;
|
|
10
|
+
const anonId = req.query.anonId || req.headers['x-anon-id'] || null;
|
|
11
|
+
|
|
12
|
+
const flagsArray = await evaluateAllForRequest({ userId, orgId, anonId });
|
|
13
|
+
const flags = flagsArrayToMap(flagsArray);
|
|
14
|
+
|
|
15
|
+
res.json({ flags });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Error evaluating feature flags:', error);
|
|
18
|
+
res.status(500).json({ error: 'Failed to evaluate feature flags' });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
exports.getPublicFlags = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const orgId = req.query.orgId || req.headers['x-org-id'] || null;
|
|
25
|
+
const anonId = req.query.anonId || req.headers['x-anon-id'] || null;
|
|
26
|
+
|
|
27
|
+
const flagsArray = await evaluateAllForRequest({ userId: null, orgId, anonId });
|
|
28
|
+
const flags = flagsArrayToMap(flagsArray);
|
|
29
|
+
|
|
30
|
+
res.json({ flags });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error evaluating public feature flags:', error);
|
|
33
|
+
res.status(500).json({ error: 'Failed to evaluate feature flags' });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const { verifyAccessToken } = require('../utils/jwt');
|
|
3
|
+
const User = require('../models/User');
|
|
4
|
+
const FormSubmission = require('../models/FormSubmission');
|
|
5
|
+
const formsService = require('../services/forms.service');
|
|
6
|
+
|
|
7
|
+
// --- Form Definition Management (Admin) ---
|
|
8
|
+
|
|
9
|
+
exports.getForms = async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const forms = await formsService.getForms();
|
|
12
|
+
res.json(forms);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
res.status(500).json({ error: error.message });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
exports.saveForm = async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
console.log('[FormsController] Saving form definition:', req.body);
|
|
21
|
+
const saved = await formsService.saveForm(req.body);
|
|
22
|
+
res.json(saved);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('[FormsController] Error saving form:', error);
|
|
25
|
+
res.status(500).json({ error: error.message, stack: process.env.NODE_ENV === 'development' ? error.stack : undefined });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.deleteForm = async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
await formsService.deleteForm(req.params.id);
|
|
32
|
+
res.json({ success: true });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
res.status(500).json({ error: error.message });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// --- Submission Management (Admin) ---
|
|
39
|
+
|
|
40
|
+
exports.adminList = async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const { formId, formKey, limit = 50, offset = 0 } = req.query;
|
|
43
|
+
// Support both formId and formKey (legacy)
|
|
44
|
+
const filterId = formId || formKey;
|
|
45
|
+
|
|
46
|
+
const result = await formsService.getSubmissions(
|
|
47
|
+
{ formId: filterId },
|
|
48
|
+
{
|
|
49
|
+
limit: parseInt(limit, 10) || 50,
|
|
50
|
+
offset: parseInt(offset, 10) || 0
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Maintain legacy response format for compatibility
|
|
55
|
+
res.json({
|
|
56
|
+
submissions: result.entries,
|
|
57
|
+
pagination: result.pagination
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Form admin list error:', error);
|
|
61
|
+
return res.status(500).json({ error: 'Failed to list submissions' });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
exports.deleteSubmission = async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
await formsService.deleteSubmission(req.params.id);
|
|
68
|
+
res.json({ success: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('[FormsController] Error deleting submission:', error);
|
|
71
|
+
res.status(500).json({ error: 'Failed to delete submission' });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// --- Public Submission ---
|
|
76
|
+
|
|
77
|
+
function parseCookieHeader(cookieHeader) {
|
|
78
|
+
const out = {};
|
|
79
|
+
if (!cookieHeader) return out;
|
|
80
|
+
const parts = cookieHeader.split(';');
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
const idx = p.indexOf('=');
|
|
83
|
+
if (idx === -1) continue;
|
|
84
|
+
const k = p.slice(0, idx).trim();
|
|
85
|
+
const v = p.slice(idx + 1).trim();
|
|
86
|
+
if (!k) continue;
|
|
87
|
+
out[k] = decodeURIComponent(v);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function tryAttachUser(req) {
|
|
93
|
+
if (req.user?._id) return;
|
|
94
|
+
const authHeader = req.headers.authorization;
|
|
95
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const token = authHeader.substring(7);
|
|
99
|
+
const decoded = verifyAccessToken(token);
|
|
100
|
+
const user = await User.findById(decoded.userId);
|
|
101
|
+
if (user) req.user = user;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAnonId(req) {
|
|
107
|
+
const headerAnon = req.get('x-anon-id');
|
|
108
|
+
if (headerAnon) return String(headerAnon).trim();
|
|
109
|
+
|
|
110
|
+
const cookies = parseCookieHeader(req.headers.cookie);
|
|
111
|
+
if (cookies.enbauges_anon_id) return String(cookies.enbauges_anon_id).trim();
|
|
112
|
+
|
|
113
|
+
const bodyAnon = req.body?.anonId;
|
|
114
|
+
if (bodyAnon) return String(bodyAnon).trim();
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeEmail(email) {
|
|
120
|
+
return String(email || '').trim().toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isValidEmail(email) {
|
|
124
|
+
if (!email) return false;
|
|
125
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
exports.submit = async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
await tryAttachUser(req);
|
|
131
|
+
|
|
132
|
+
const formKey = String(req.params.formId || req.body?.formKey || '').trim();
|
|
133
|
+
const fields = req.body; // In POST forms, fields are the body itself or nested in fields
|
|
134
|
+
|
|
135
|
+
if (!formKey) {
|
|
136
|
+
return res.status(400).json({ error: 'formKey is required' });
|
|
137
|
+
}
|
|
138
|
+
if (!fields || typeof fields !== 'object') {
|
|
139
|
+
return res.status(400).json({ error: 'fields is required' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (formKey === 'contact') {
|
|
143
|
+
const email = normalizeEmail(fields.email);
|
|
144
|
+
const message = String(fields.message || '').trim();
|
|
145
|
+
if (!isValidEmail(email)) {
|
|
146
|
+
return res.status(400).json({ error: 'Email invalide', field: 'email' });
|
|
147
|
+
}
|
|
148
|
+
if (!message || message.length < 5) {
|
|
149
|
+
return res.status(400).json({ error: 'Message trop court', field: 'message' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let actorType = 'anonymous';
|
|
154
|
+
let actorId = getAnonId(req);
|
|
155
|
+
let userId = null;
|
|
156
|
+
|
|
157
|
+
if (req.user?._id) {
|
|
158
|
+
actorType = 'user';
|
|
159
|
+
actorId = String(req.user._id);
|
|
160
|
+
userId = req.user._id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!actorId) {
|
|
164
|
+
actorId = crypto.randomUUID();
|
|
165
|
+
res.cookie('enbauges_anon_id', actorId, {
|
|
166
|
+
httpOnly: false,
|
|
167
|
+
sameSite: 'lax',
|
|
168
|
+
maxAge: 1000 * 60 * 60 * 24 * 365,
|
|
169
|
+
path: '/',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const meta = {
|
|
174
|
+
ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress || null,
|
|
175
|
+
userAgent: req.get('user-agent') || null,
|
|
176
|
+
referer: req.get('referer') || null,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const doc = await formsService.submitForm(formKey, fields, {
|
|
180
|
+
...meta,
|
|
181
|
+
actorType,
|
|
182
|
+
actorId,
|
|
183
|
+
userId
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const formConfig = await formsService.getFormById(formKey);
|
|
187
|
+
if (formConfig?.successUrl) {
|
|
188
|
+
return res.redirect(formConfig.successUrl);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return res.status(201).json({ ok: true, id: String(doc._id) });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('Form submit error:', error);
|
|
194
|
+
return res.status(500).json({ error: 'Failed to submit' });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
exports.adminList = async (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const { formKey, limit = 50, offset = 0 } = req.query;
|
|
201
|
+
const query = {};
|
|
202
|
+
if (formKey) query.formKey = formKey;
|
|
203
|
+
|
|
204
|
+
const submissions = await FormSubmission.find(query)
|
|
205
|
+
.sort({ createdAt: -1 })
|
|
206
|
+
.limit(parseInt(limit))
|
|
207
|
+
.skip(parseInt(offset))
|
|
208
|
+
.lean();
|
|
209
|
+
|
|
210
|
+
const total = await FormSubmission.countDocuments(query);
|
|
211
|
+
|
|
212
|
+
return res.json({ submissions, pagination: { total, limit: parseInt(limit), offset: parseInt(offset) } });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Form admin list error:', error);
|
|
215
|
+
return res.status(500).json({ error: 'Failed to list submissions' });
|
|
216
|
+
}
|
|
217
|
+
};
|