backend-manager 5.0.102 → 5.0.104
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/CLAUDE.md +59 -0
- package/README.md +10 -0
- package/TODO-MARKETING.md +53 -0
- package/package.json +1 -1
- package/src/cli/commands/auth.js +226 -0
- package/src/cli/commands/firebase-init.js +121 -0
- package/src/cli/commands/firestore.js +261 -0
- package/src/cli/commands/index.js +2 -0
- package/src/cli/index.js +16 -0
- package/src/manager/libraries/payment-processors/stripe.js +37 -0
- package/src/manager/routes/payments/cancel/post.js +66 -0
- package/src/manager/routes/payments/cancel/processors/stripe.js +22 -0
- package/src/manager/routes/payments/cancel/processors/test.js +93 -0
- package/src/manager/routes/payments/intent/processors/stripe.js +1 -30
- package/src/manager/routes/payments/portal/post.js +54 -0
- package/src/manager/routes/payments/portal/processors/stripe.js +62 -0
- package/src/manager/routes/payments/portal/processors/test.js +18 -0
- package/src/manager/routes/user/delete.js +2 -1
- package/src/manager/schemas/payments/cancel/post.js +18 -0
- package/src/manager/schemas/payments/portal/post.js +10 -0
- package/src/test/runner.js +18 -1
- package/src/test/test-accounts.js +92 -0
- package/templates/firestore.rules +1 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +79 -0
- package/test/helpers/stripe-to-unified-one-time.js +304 -0
- package/test/routes/payments/cancel.js +124 -0
- package/test/routes/payments/intent.js +7 -14
- package/test/routes/payments/portal.js +89 -0
- package/src/manager/routes/forms/delete.js +0 -37
- package/src/manager/routes/forms/get.js +0 -46
- package/src/manager/routes/forms/post.js +0 -45
- package/src/manager/routes/forms/public/get.js +0 -37
- package/src/manager/routes/forms/put.js +0 -52
- package/src/manager/schemas/forms/delete.js +0 -6
- package/src/manager/schemas/forms/get.js +0 -6
- package/src/manager/schemas/forms/post.js +0 -9
- package/src/manager/schemas/forms/public/get.js +0 -6
- package/src/manager/schemas/forms/put.js +0 -10
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: POST /payments/cancel - Validation errors
|
|
3
|
+
* Tests rejection cases before any processor call is made.
|
|
4
|
+
* See test/events/payments/journey-payments-cancel-endpoint.js for the full end-to-end journey.
|
|
5
|
+
*/
|
|
6
|
+
module.exports = {
|
|
7
|
+
description: 'Payment cancel endpoint: validation errors',
|
|
8
|
+
type: 'group',
|
|
9
|
+
timeout: 15000,
|
|
10
|
+
|
|
11
|
+
tests: [
|
|
12
|
+
{
|
|
13
|
+
name: 'rejects-unauthenticated',
|
|
14
|
+
async run({ http, assert }) {
|
|
15
|
+
const response = await http.as('none').post('payments/cancel', {
|
|
16
|
+
confirmed: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert.isError(response, 401, 'Should reject unauthenticated request');
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
name: 'rejects-missing-confirmed',
|
|
25
|
+
async run({ http, assert }) {
|
|
26
|
+
const response = await http.as('basic').post('payments/cancel', {
|
|
27
|
+
reason: 'Too expensive',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.isError(response, 400, 'Should reject missing confirmed field');
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
name: 'rejects-basic-user',
|
|
36
|
+
async run({ http, assert }) {
|
|
37
|
+
const response = await http.as('basic').post('payments/cancel', {
|
|
38
|
+
confirmed: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.isError(response, 400, 'Should reject basic user with no paid subscription');
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
name: 'rejects-no-processor-or-resource-id',
|
|
47
|
+
async run({ http, assert }) {
|
|
48
|
+
// cancel-no-processor starts with payment.processor=null
|
|
49
|
+
const response = await http.as('cancel-no-processor').post('payments/cancel', {
|
|
50
|
+
confirmed: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.isError(response, 400, 'Should reject when no processor or resourceId is set');
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
name: 'rejects-already-pending-cancellation',
|
|
59
|
+
async run({ http, assert }) {
|
|
60
|
+
// cancel-already-pending starts with cancellation.pending=true
|
|
61
|
+
const response = await http.as('cancel-already-pending').post('payments/cancel', {
|
|
62
|
+
confirmed: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.isError(response, 400, 'Should reject when cancellation already pending');
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
name: 'rejects-unknown-processor',
|
|
71
|
+
async run({ http, assert }) {
|
|
72
|
+
// cancel-unknown-processor starts with processor='unknown-processor'
|
|
73
|
+
const response = await http.as('cancel-unknown-processor').post('payments/cancel', {
|
|
74
|
+
confirmed: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.isError(response, 400, 'Should reject unknown processor');
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
name: 'succeeds-with-test-processor',
|
|
83
|
+
async run({ http, assert, config, accounts, firestore, waitFor }) {
|
|
84
|
+
const uid = accounts['route-cancel-success'].uid;
|
|
85
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices?.monthly);
|
|
86
|
+
|
|
87
|
+
// Step 1: Create a test subscription intent to set up a proper paid subscription
|
|
88
|
+
const intentResponse = await http.as('route-cancel-success').post('payments/intent', {
|
|
89
|
+
processor: 'test',
|
|
90
|
+
productId: paidProduct.id,
|
|
91
|
+
frequency: 'monthly',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.isSuccess(intentResponse, 'Intent should succeed');
|
|
95
|
+
|
|
96
|
+
// Wait for the auto-webhook to activate the subscription
|
|
97
|
+
await waitFor(async () => {
|
|
98
|
+
const userDoc = await firestore.get(`users/${uid}`);
|
|
99
|
+
return userDoc?.subscription?.payment?.processor === 'test'
|
|
100
|
+
&& userDoc?.subscription?.payment?.resourceId
|
|
101
|
+
&& userDoc?.subscription?.status === 'active';
|
|
102
|
+
}, 15000, 500);
|
|
103
|
+
|
|
104
|
+
// Step 2: Call the cancel endpoint
|
|
105
|
+
const cancelResponse = await http.as('route-cancel-success').post('payments/cancel', {
|
|
106
|
+
confirmed: true,
|
|
107
|
+
reason: 'Too expensive',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.isSuccess(cancelResponse, 'Cancel should succeed');
|
|
111
|
+
assert.equal(cancelResponse.data.success, true, 'Should return success: true');
|
|
112
|
+
|
|
113
|
+
// Step 3: Verify cancellation.pending was set via the webhook pipeline
|
|
114
|
+
await waitFor(async () => {
|
|
115
|
+
const userDoc = await firestore.get(`users/${uid}`);
|
|
116
|
+
return userDoc?.subscription?.cancellation?.pending === true;
|
|
117
|
+
}, 15000, 500);
|
|
118
|
+
|
|
119
|
+
const userDoc = await firestore.get(`users/${uid}`);
|
|
120
|
+
assert.equal(userDoc?.subscription?.cancellation?.pending, true, 'Cancellation should be pending');
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
@@ -116,9 +116,10 @@ module.exports = {
|
|
|
116
116
|
{
|
|
117
117
|
name: 'succeeds-with-test-processor',
|
|
118
118
|
async run({ http, assert, config, firestore, accounts, waitFor }) {
|
|
119
|
+
const uid = accounts['journey-payments-intent'].uid;
|
|
119
120
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
120
121
|
|
|
121
|
-
const response = await http.as('
|
|
122
|
+
const response = await http.as('journey-payments-intent').post('payments/intent', {
|
|
122
123
|
processor: 'test',
|
|
123
124
|
productId: paidProduct.id,
|
|
124
125
|
frequency: 'monthly',
|
|
@@ -137,30 +138,26 @@ module.exports = {
|
|
|
137
138
|
assert.equal(intentDoc.processor, 'test', 'Processor should be test');
|
|
138
139
|
assert.equal(intentDoc.productId, paidProduct.id, 'Product should match');
|
|
139
140
|
|
|
140
|
-
//
|
|
141
|
+
// Wait for auto-webhook to process and activate the subscription
|
|
141
142
|
await waitFor(async () => {
|
|
142
|
-
const userDoc = await firestore.get(`users/${
|
|
143
|
+
const userDoc = await firestore.get(`users/${uid}`);
|
|
143
144
|
return userDoc?.subscription?.product?.id === paidProduct.id;
|
|
144
145
|
}, 15000, 500).catch(() => {});
|
|
145
|
-
|
|
146
|
-
await firestore.set(`users/${accounts.basic.uid}`, {
|
|
147
|
-
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
148
|
-
}, { merge: true });
|
|
149
146
|
},
|
|
150
147
|
},
|
|
151
148
|
|
|
152
149
|
{
|
|
153
150
|
name: 'downgrades-trial-for-user-with-history',
|
|
154
151
|
async run({ http, assert, config, accounts, firestore, waitFor }) {
|
|
152
|
+
const uid = accounts['journey-payments-intent-trial'].uid;
|
|
155
153
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
156
|
-
const uid = accounts.basic.uid;
|
|
157
154
|
const orderDocPath = `payments-orders/_test-order-history-${uid}`;
|
|
158
155
|
|
|
159
156
|
// Create fake subscription history so user is ineligible for trial
|
|
160
157
|
await firestore.set(orderDocPath, { owner: uid, type: 'subscription', processor: 'test', status: 'cancelled' });
|
|
161
158
|
|
|
162
159
|
try {
|
|
163
|
-
const response = await http.as('
|
|
160
|
+
const response = await http.as('journey-payments-intent-trial').post('payments/intent', {
|
|
164
161
|
processor: 'test',
|
|
165
162
|
productId: paidProduct.id,
|
|
166
163
|
frequency: 'monthly',
|
|
@@ -174,15 +171,11 @@ module.exports = {
|
|
|
174
171
|
const intentDoc = await firestore.get(`payments-intents/${response.data.orderId}`);
|
|
175
172
|
assert.equal(intentDoc.trial, false, 'Trial should be false (downgraded)');
|
|
176
173
|
|
|
177
|
-
//
|
|
174
|
+
// Wait for auto-webhook to activate the subscription
|
|
178
175
|
await waitFor(async () => {
|
|
179
176
|
const userDoc = await firestore.get(`users/${uid}`);
|
|
180
177
|
return userDoc?.subscription?.product?.id === paidProduct.id;
|
|
181
178
|
}, 15000, 500).catch(() => {});
|
|
182
|
-
|
|
183
|
-
await firestore.set(`users/${uid}`, {
|
|
184
|
-
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
185
|
-
}, { merge: true });
|
|
186
179
|
} finally {
|
|
187
180
|
await firestore.delete(orderDocPath);
|
|
188
181
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: POST /payments/portal - Validation errors
|
|
3
|
+
* Tests rejection cases before any processor call is made.
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
description: 'Payment portal endpoint: validation errors',
|
|
7
|
+
type: 'group',
|
|
8
|
+
timeout: 15000,
|
|
9
|
+
|
|
10
|
+
tests: [
|
|
11
|
+
{
|
|
12
|
+
name: 'rejects-unauthenticated',
|
|
13
|
+
async run({ http, assert }) {
|
|
14
|
+
const response = await http.as('none').post('payments/portal', {
|
|
15
|
+
returnUrl: 'https://example.com/account',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.isError(response, 401, 'Should reject unauthenticated request');
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
name: 'rejects-basic-user',
|
|
24
|
+
async run({ http, assert }) {
|
|
25
|
+
const response = await http.as('basic').post('payments/portal', {
|
|
26
|
+
returnUrl: 'https://example.com/account',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.isError(response, 400, 'Should reject basic user with no paid subscription');
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
name: 'rejects-no-processor',
|
|
35
|
+
async run({ http, assert }) {
|
|
36
|
+
// portal-no-processor starts with payment.processor=null
|
|
37
|
+
const response = await http.as('portal-no-processor').post('payments/portal', {
|
|
38
|
+
returnUrl: 'https://example.com/account',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.isError(response, 400, 'Should reject when no processor is set');
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
name: 'rejects-unknown-processor',
|
|
47
|
+
async run({ http, assert }) {
|
|
48
|
+
// portal-unknown-processor starts with processor='unknown-processor'
|
|
49
|
+
const response = await http.as('portal-unknown-processor').post('payments/portal', {
|
|
50
|
+
returnUrl: 'https://example.com/account',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.isError(response, 400, 'Should reject unknown processor');
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
name: 'succeeds-with-test-processor',
|
|
59
|
+
async run({ http, assert, config, accounts, firestore, waitFor }) {
|
|
60
|
+
const uid = accounts['journey-payments-portal-route'].uid;
|
|
61
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices?.monthly);
|
|
62
|
+
|
|
63
|
+
// Set up a paid subscription with the test processor
|
|
64
|
+
const intentResponse = await http.as('journey-payments-portal-route').post('payments/intent', {
|
|
65
|
+
processor: 'test',
|
|
66
|
+
productId: paidProduct.id,
|
|
67
|
+
frequency: 'monthly',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.isSuccess(intentResponse, 'Intent should succeed');
|
|
71
|
+
|
|
72
|
+
// Wait for the auto-webhook to activate the subscription
|
|
73
|
+
await waitFor(async () => {
|
|
74
|
+
const userDoc = await firestore.get(`users/${uid}`);
|
|
75
|
+
return userDoc?.subscription?.payment?.processor === 'test'
|
|
76
|
+
&& userDoc?.subscription?.status === 'active';
|
|
77
|
+
}, 15000, 500);
|
|
78
|
+
|
|
79
|
+
// Call the portal endpoint
|
|
80
|
+
const portalResponse = await http.as('journey-payments-portal-route').post('payments/portal', {
|
|
81
|
+
returnUrl: 'https://example.com/account',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.isSuccess(portalResponse, 'Portal should succeed');
|
|
85
|
+
assert.ok(portalResponse.data.url, 'Should return a URL');
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DELETE /forms - Delete a form
|
|
3
|
-
* Requires authentication and ownership.
|
|
4
|
-
*/
|
|
5
|
-
module.exports = async ({ assistant, user, settings, analytics, libraries }) => {
|
|
6
|
-
const { admin } = libraries;
|
|
7
|
-
|
|
8
|
-
// Require authentication
|
|
9
|
-
if (!user.authenticated) {
|
|
10
|
-
return assistant.respond('Authentication required', { code: 401 });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (!settings.id) {
|
|
14
|
-
return assistant.respond('Missing required parameter: id', { code: 400 });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const uid = user.auth.uid;
|
|
18
|
-
const formRef = admin.firestore().doc(`forms/${settings.id}`);
|
|
19
|
-
const doc = await formRef.get();
|
|
20
|
-
|
|
21
|
-
if (!doc.exists) {
|
|
22
|
-
return assistant.respond('Form not found', { code: 404 });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Ownership check
|
|
26
|
-
if (doc.data().owner !== uid) {
|
|
27
|
-
return assistant.respond('Not authorized to delete this form', { code: 403 });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
await formRef.delete();
|
|
31
|
-
|
|
32
|
-
assistant.log(`Deleted form ${settings.id} for user ${uid}`);
|
|
33
|
-
|
|
34
|
-
analytics.event('forms', { action: 'delete' });
|
|
35
|
-
|
|
36
|
-
return assistant.respond({ data: { deleted: true } });
|
|
37
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /forms - Get a single form or list all forms for the authenticated user
|
|
3
|
-
* Requires authentication.
|
|
4
|
-
* - With ?id=xxx: returns a single form (with ownership check)
|
|
5
|
-
* - Without id: returns all forms owned by the user
|
|
6
|
-
*/
|
|
7
|
-
module.exports = async ({ assistant, user, settings, analytics, libraries }) => {
|
|
8
|
-
const { admin } = libraries;
|
|
9
|
-
|
|
10
|
-
// Require authentication
|
|
11
|
-
if (!user.authenticated) {
|
|
12
|
-
return assistant.respond('Authentication required', { code: 401 });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const uid = user.auth.uid;
|
|
16
|
-
|
|
17
|
-
// Single form
|
|
18
|
-
if (settings.id) {
|
|
19
|
-
const doc = await admin.firestore().doc(`forms/${settings.id}`).get();
|
|
20
|
-
|
|
21
|
-
if (!doc.exists) {
|
|
22
|
-
return assistant.respond('Form not found', { code: 404 });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (doc.data().owner !== uid) {
|
|
26
|
-
return assistant.respond('Not authorized to view this form', { code: 403 });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
analytics.event('forms', { action: 'get' });
|
|
30
|
-
|
|
31
|
-
return assistant.respond({ data: { form: doc.data() } });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// List all forms
|
|
35
|
-
const snapshot = await admin.firestore()
|
|
36
|
-
.collection('forms')
|
|
37
|
-
.where('owner', '==', uid)
|
|
38
|
-
.orderBy('created.timestampUNIX', 'desc')
|
|
39
|
-
.get();
|
|
40
|
-
|
|
41
|
-
const forms = snapshot.docs.map(doc => doc.data());
|
|
42
|
-
|
|
43
|
-
analytics.event('forms', { action: 'list' });
|
|
44
|
-
|
|
45
|
-
return assistant.respond({ data: { forms } });
|
|
46
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /forms - Create a new form
|
|
3
|
-
* Requires authentication. Creates a Firestore doc in the `forms` collection.
|
|
4
|
-
*/
|
|
5
|
-
module.exports = async ({ assistant, Manager, user, settings, analytics, libraries }) => {
|
|
6
|
-
const { admin } = libraries;
|
|
7
|
-
|
|
8
|
-
// Require authentication
|
|
9
|
-
if (!user.authenticated) {
|
|
10
|
-
return assistant.respond('Authentication required', { code: 401 });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const uid = user.auth.uid;
|
|
14
|
-
const now = new Date().toISOString();
|
|
15
|
-
const nowUnix = Date.now();
|
|
16
|
-
|
|
17
|
-
// Generate a new document reference
|
|
18
|
-
const docRef = admin.firestore().collection('forms').doc();
|
|
19
|
-
|
|
20
|
-
const form = {
|
|
21
|
-
id: docRef.id,
|
|
22
|
-
owner: uid,
|
|
23
|
-
name: settings.name || 'Untitled Form',
|
|
24
|
-
description: settings.description || '',
|
|
25
|
-
settings: settings.settings || {},
|
|
26
|
-
pages: settings.pages || [],
|
|
27
|
-
created: {
|
|
28
|
-
timestamp: now,
|
|
29
|
-
timestampUNIX: nowUnix,
|
|
30
|
-
},
|
|
31
|
-
edited: {
|
|
32
|
-
timestamp: now,
|
|
33
|
-
timestampUNIX: nowUnix,
|
|
34
|
-
},
|
|
35
|
-
metadata: Manager.Metadata().set({ tag: 'forms/post' }),
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
await docRef.set(form);
|
|
39
|
-
|
|
40
|
-
assistant.log(`Created form ${docRef.id} for user ${uid}`);
|
|
41
|
-
|
|
42
|
-
analytics.event('forms', { action: 'create' });
|
|
43
|
-
|
|
44
|
-
return assistant.respond({ data: { id: docRef.id, form } });
|
|
45
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /forms/public - Get a public form by ID
|
|
3
|
-
* No authentication required. Only returns forms with settings.public = true.
|
|
4
|
-
*/
|
|
5
|
-
module.exports = async ({ assistant, settings, analytics, libraries }) => {
|
|
6
|
-
const { admin } = libraries;
|
|
7
|
-
|
|
8
|
-
if (!settings.id) {
|
|
9
|
-
return assistant.respond('Missing required parameter: id', { code: 400 });
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const doc = await admin.firestore().doc(`forms/${settings.id}`).get();
|
|
13
|
-
|
|
14
|
-
if (!doc.exists) {
|
|
15
|
-
return assistant.respond('Form not found', { code: 404 });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const form = doc.data();
|
|
19
|
-
|
|
20
|
-
// Only allow access to public forms
|
|
21
|
-
if (!form.settings?.public) {
|
|
22
|
-
return assistant.respond('Form not found', { code: 404 });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Strip sensitive fields
|
|
26
|
-
const publicForm = {
|
|
27
|
-
id: form.id,
|
|
28
|
-
name: form.name,
|
|
29
|
-
description: form.description,
|
|
30
|
-
settings: form.settings,
|
|
31
|
-
pages: form.pages,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
analytics.event('forms/public', { action: 'get' });
|
|
35
|
-
|
|
36
|
-
return assistant.respond({ payload: publicForm });
|
|
37
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PUT /forms - Update an existing form
|
|
3
|
-
* Requires authentication and ownership.
|
|
4
|
-
*/
|
|
5
|
-
module.exports = async ({ assistant, Manager, user, settings, analytics, libraries }) => {
|
|
6
|
-
const { admin } = libraries;
|
|
7
|
-
|
|
8
|
-
// Require authentication
|
|
9
|
-
if (!user.authenticated) {
|
|
10
|
-
return assistant.respond('Authentication required', { code: 401 });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (!settings.id) {
|
|
14
|
-
return assistant.respond('Missing required parameter: id', { code: 400 });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const uid = user.auth.uid;
|
|
18
|
-
const formRef = admin.firestore().doc(`forms/${settings.id}`);
|
|
19
|
-
const doc = await formRef.get();
|
|
20
|
-
|
|
21
|
-
if (!doc.exists) {
|
|
22
|
-
return assistant.respond('Form not found', { code: 404 });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Ownership check
|
|
26
|
-
if (doc.data().owner !== uid) {
|
|
27
|
-
return assistant.respond('Not authorized to edit this form', { code: 403 });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const now = new Date().toISOString();
|
|
31
|
-
const nowUnix = Date.now();
|
|
32
|
-
|
|
33
|
-
const updates = {
|
|
34
|
-
name: settings.name,
|
|
35
|
-
description: settings.description,
|
|
36
|
-
settings: settings.settings,
|
|
37
|
-
pages: settings.pages,
|
|
38
|
-
edited: {
|
|
39
|
-
timestamp: now,
|
|
40
|
-
timestampUNIX: nowUnix,
|
|
41
|
-
},
|
|
42
|
-
metadata: Manager.Metadata().set({ tag: 'forms/put' }),
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
await formRef.update(updates);
|
|
46
|
-
|
|
47
|
-
assistant.log(`Updated form ${settings.id} for user ${uid}`);
|
|
48
|
-
|
|
49
|
-
analytics.event('forms', { action: 'update' });
|
|
50
|
-
|
|
51
|
-
return assistant.respond({ data: { id: settings.id } });
|
|
52
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema for POST /forms (create)
|
|
3
|
-
*/
|
|
4
|
-
module.exports = () => ({
|
|
5
|
-
name: { types: ['string'], default: 'Untitled Form' },
|
|
6
|
-
description: { types: ['string'], default: '' },
|
|
7
|
-
settings: { types: ['object'], default: {} },
|
|
8
|
-
pages: { types: ['array'], default: [] },
|
|
9
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema for PUT /forms (update)
|
|
3
|
-
*/
|
|
4
|
-
module.exports = () => ({
|
|
5
|
-
id: { types: ['string'], default: undefined, required: true },
|
|
6
|
-
name: { types: ['string'], default: undefined },
|
|
7
|
-
description: { types: ['string'], default: undefined },
|
|
8
|
-
settings: { types: ['object'], default: undefined },
|
|
9
|
-
pages: { types: ['array'], default: undefined },
|
|
10
|
-
});
|