Skip to content

Commit 0448670

Browse files
committed
CCP-4720: public form submission sharing option
1 parent 6c5f6da commit 0448670

28 files changed

Lines changed: 1312 additions & 75 deletions

File tree

.devcontainer/chefs_local/local.sample.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
},
6767
"encryption": {
6868
"proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7"
69-
}
69+
},
70+
"submissionTokenKey": "REPLACE_ME_WITH_32_PLUS_RANDOM_BYTES_e.g._openssl_rand_-hex_32"
7071
},
7172
"eventStreamService": {
7273
"servers": "localhost:4222,localhost:4223,localhost:4224",

.github/actions/deploy-to-environment/action.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,21 @@ runs:
9191
9292
- name: Deploy App Secret
9393
shell: bash
94-
run: >-
95-
oc get --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} secret chefs-${{ inputs.job_name }}-secret || oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/app.secret.yaml -p INSTANCE=${{ inputs.job_name }} | oc create --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f -
94+
env:
95+
NAMESPACE: ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }}
96+
JOB_NAME: ${{ inputs.job_name }}
97+
run: |
98+
set -euo pipefail
99+
100+
SECRET_NAME="chefs-${JOB_NAME}-secret"
101+
if ! oc get --namespace "${NAMESPACE}" secret "${SECRET_NAME}" >/dev/null 2>&1; then
102+
oc process --namespace "${NAMESPACE}" -f openshift/app.secret.yaml -p INSTANCE="${JOB_NAME}" | oc create --namespace "${NAMESPACE}" -f -
103+
fi
104+
105+
SUBMISSION_TOKEN_KEY="$(oc get --namespace "${NAMESPACE}" secret "${SECRET_NAME}" -o jsonpath='{.data.submissiontokenkey}' | base64 -d || true)"
106+
if [[ ${#SUBMISSION_TOKEN_KEY} -lt 32 ]]; then
107+
oc patch --namespace "${NAMESPACE}" secret "${SECRET_NAME}" --type merge -p "{\"stringData\":{\"submissiontokenkey\":\"$(openssl rand -hex 32)\"}}"
108+
fi
96109
97110
- name: Deploy app ConfigMaps
98111
shell: bash

app/config/custom-environment-variables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
"port": "SERVER_PORT",
6363
"encryption": {
6464
"proxy": "SERVER_ENCRYPTION_PROXY"
65-
}
65+
},
66+
"submissionTokenKey": "SUBMISSION_TOKEN_KEY",
67+
"submissionTokenTtlMinutes": "SUBMISSION_TOKEN_TTL_MINUTES"
6668
},
6769
"eventStreamService": {
6870
"servers": "EVENTSTREAMSERVICE_SERVERS",

app/config/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
},
6767
"encryption": {
6868
"proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7"
69-
}
69+
},
70+
"submissionTokenTtlMinutes": "15"
7071
},
7172
"eventStreamService": {
7273
"servers": "localhost:4222,localhost:4223,localhost:4224",

app/config/test.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"oidc": {
3030
"clientSecret": "password"
3131
},
32-
"logLevel": "silent"
32+
"logLevel": "silent",
33+
"submissionTokenKey": "test-only-submission-token-key-32-bytes-min-do-not-use-anywhere-else"
3334
},
3435
"serviceClient": {
3536
"commonServices": {

app/frontend/src/components/designer/FormDesigner.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ async function schemaCreateNew() {
409409
enableSubmitterRevision: form.value.enableSubmitterRevision,
410410
showAssigneeInSubmissionsTable: form.value.showAssigneeInSubmissionsTable,
411411
showSubmissionConfirmation: form.value.showSubmissionConfirmation,
412+
enableSubmitterEmailReceipt: form.value.enableSubmitterEmailReceipt,
413+
enableSubmissionUrlSharing: form.value.enableSubmissionUrlSharing,
412414
submissionReceivedEmails: form.value.submissionReceivedEmails,
413415
reminder_enabled: false,
414416
deploymentLevel: form.value.deploymentLevel,

app/frontend/src/components/designer/settings/FormSubmissionSettings.vue

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
<script setup>
22
import { storeToRefs } from 'pinia';
3-
import { ref } from 'vue';
3+
import { computed, ref } from 'vue';
44
import { useI18n } from 'vue-i18n';
55
66
import { useFormStore } from '~/store/form';
7-
import { Regex } from '~/utils/constants';
7+
import { IdentityMode, Regex } from '~/utils/constants';
88
99
const { t, locale } = useI18n({ useScope: 'global' });
1010
1111
const { form, isRTL } = storeToRefs(useFormStore());
1212
13+
const isPublicForm = computed(
14+
() => form.value.userType === IdentityMode.PUBLIC
15+
);
16+
// Returning undefined (rather than false) lets the surrounding <v-form disabled>
17+
// cascade through; an explicit `false` would override the parent's view-only
18+
// state on a published form and leave only this checkbox toggleable.
19+
const sharingToggleDisabled = computed(() =>
20+
isPublicForm.value ? undefined : true
21+
);
22+
const urlSharingDisabled = computed(
23+
() => isPublicForm.value && form.value.enableSubmissionUrlSharing === false
24+
);
25+
26+
// The "email me a copy" email contains a link back to /form/success?s=<id>.
27+
// When URL sharing is off that link opens in a browser with no access token
28+
// and renders the static block, so the email is misleading; worse, forwarding
29+
// the email is the exact URL-leak vector turning sharing off is meant to close.
30+
// Auto-uncheck on disable so the form owner never ships a sharing-off form
31+
// with a promise the receipt can't keep.
32+
function enableSubmissionUrlSharingChanged(checked) {
33+
form.value.enableSubmissionUrlSharing = checked;
34+
if (!checked) {
35+
form.value.enableSubmitterEmailReceipt = false;
36+
}
37+
}
38+
1339
/* c8 ignore start */
1440
const emailArrayRules = ref([
1541
(v) =>
@@ -31,6 +57,41 @@ const emailArrayRules = ref([
3157
{{ $t('trans.formSettings.afterSubmission') }}
3258
</span></template
3359
>
60+
<!-- Enable Submission URL Sharing (only meaningful on public forms) -->
61+
<v-checkbox
62+
:model-value="form.enableSubmissionUrlSharing !== false"
63+
:disabled="sharingToggleDisabled"
64+
hide-details="auto"
65+
class="my-0"
66+
data-test="enableSubmissionUrlSharingCheckbox"
67+
:class="{ 'dir-rtl': isRTL }"
68+
@update:model-value="enableSubmissionUrlSharingChanged"
69+
>
70+
<template #label>
71+
<div :class="{ 'mr-2': isRTL }">
72+
<span
73+
:lang="locale"
74+
v-html="$t('trans.formSettings.enableSubmissionUrlSharing')"
75+
/>
76+
<v-tooltip location="bottom">
77+
<template #activator="{ props }">
78+
<v-icon
79+
color="primary"
80+
class="ml-3"
81+
:class="{ 'mr-2': isRTL }"
82+
v-bind="props"
83+
icon="mdi:mdi-help-circle-outline"
84+
/>
85+
</template>
86+
<span
87+
:lang="locale"
88+
v-html="$t('trans.formSettings.enableSubmissionUrlSharingTip')"
89+
/>
90+
</v-tooltip>
91+
</div>
92+
</template>
93+
</v-checkbox>
94+
3495
<v-checkbox
3596
v-model="form.showSubmissionConfirmation"
3697
hide-details="auto"
@@ -40,9 +101,10 @@ const emailArrayRules = ref([
40101
>
41102
<template #label>
42103
<div :class="{ 'mr-2': isRTL }">
43-
<span :lang="locale">
44-
{{ $t('trans.formSettings.submissionConfirmation') }}</span
45-
>
104+
<span
105+
:lang="locale"
106+
v-html="$t('trans.formSettings.submissionConfirmation')"
107+
/>
46108
<v-tooltip location="bottom">
47109
<template #activator="{ props }">
48110
<v-icon
@@ -53,20 +115,43 @@ const emailArrayRules = ref([
53115
icon="mdi:mdi-help-circle-outline"
54116
/>
55117
</template>
56-
<span>
57-
<span
58-
:lang="locale"
59-
v-html="$t('trans.formSettings.submissionConfirmationToolTip')"
118+
<span
119+
:lang="locale"
120+
v-html="$t('trans.formSettings.submissionConfirmationToolTip')"
121+
/>
122+
</v-tooltip>
123+
</div>
124+
</template>
125+
</v-checkbox>
126+
127+
<v-checkbox
128+
v-model="form.enableSubmitterEmailReceipt"
129+
:disabled="urlSharingDisabled"
130+
hide-details="auto"
131+
data-test="enableSubmitterEmailReceiptCheckbox"
132+
class="my-0"
133+
:class="{ 'dir-rtl': isRTL }"
134+
>
135+
<template #label>
136+
<div :class="{ 'mr-2': isRTL }">
137+
<span
138+
:lang="locale"
139+
v-html="$t('trans.formSettings.enableSubmitterEmailReceipt')"
140+
/>
141+
<v-tooltip location="bottom">
142+
<template #activator="{ props }">
143+
<v-icon
144+
color="primary"
145+
class="ml-3"
146+
:class="{ 'mr-2': isRTL }"
147+
v-bind="props"
148+
icon="mdi:mdi-help-circle-outline"
60149
/>
61-
<ul>
62-
<li :lang="locale">
63-
{{ $t('trans.formSettings.theConfirmationID') }}
64-
</li>
65-
<li :lang="locale">
66-
{{ $t('trans.formSettings.infoB') }}
67-
</li>
68-
</ul>
69-
</span>
150+
</template>
151+
<span
152+
:lang="locale"
153+
v-html="$t('trans.formSettings.enableSubmitterEmailReceiptTip')"
154+
/>
70155
</v-tooltip>
71156
</div>
72157
</template>

app/frontend/src/internationalization/trans/chefs/en/en.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@
191191
"formSettings": {
192192
"pressToAddMultiEmail": "Press <kbd>enter</kbd> or <kbd>,</kbd> or <kbd>space</kbd> to add multiple email addresses",
193193
"allowMultiDraft": "Allow <strong> multiple draft</strong> upload",
194+
"enableSubmissionUrlSharing": "Enable <strong>Submission URL Sharing</strong>",
195+
"enableSubmissionUrlSharingTip": "When unchecked, the submitter can still view and print their submission while the success page stays open,<br>but anyone else opening the URL sees only the confirmation message.<br>The email-receipt option is turned off because forwarding the receipt would defeat this protection.<br>You may want to adjust your post-confirmation message to include any details the submitter might need later.",
194196
"formTitle": "Form Title",
195197
"formDescription": "Form Description",
196198
"formAccess": "Form Access",
@@ -211,16 +213,18 @@
211213
"experimental": "Experimental",
212214
"learnMore": "Learn more",
213215
"afterSubmission": "After Submission",
214-
"submissionConfirmation": "Show the submission confirmation details",
216+
"submissionConfirmation": "Show the <strong>Confirmation ID</strong>",
215217
"theConfirmationID": "the Confirmation ID",
216218
"infoB": "the option for the user to email themselves a submission confirmation",
219+
"enableSubmitterEmailReceipt": "Let submitters <strong>email themselves a copy</strong> of their submission",
220+
"enableSubmitterEmailReceiptTip": "When checked, the success page shows an option for the submitter to email themselves a confirmation receipt.<br>Disabled when Form Submission URL Privacy is on, because the email link routes back to the success page<br>and would not work for the recipient.",
217221
"loginRequired": "Log-in Required",
218222
"canSaveAndEditDraftLabel": "Submitters can <strong>Save and Edit Drafts</strong>",
219223
"canUpdateStatusAsReviewer": "Reviewers can <strong>Update the Status</strong> of this form (i.e. Submitted, Assigned, Completed)",
220224
"enableSubmitterRevision": "Submitters can <strong>Revise their own submissions</strong> and resubmit",
221225
"displayAssigneeColumn": "Display assignee column for reviewers",
222226
"submitterCanCopyExistingSubmissn": "Submitters can<strong> Copy an existing submission</strong>",
223-
"submissionConfirmationToolTip": "Selecting this option controls what the submitting user of this form will see on successful submission. <br /> If checked, it will display",
227+
"submissionConfirmationToolTip": "When checked, the success page displays the unique Confirmation ID for the submission.",
224228
"emailNotificatnToTeam": "Send my team a notification email",
225229
"emailNotificatnToTeamToolTip": "Send a notification to your specified email address when any user submits this form",
226230
"notificationEmailAddrs": "Notification Email Addresses",

app/frontend/src/services/formService.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
import { appAxios } from '~/services/interceptors';
22
import { ApiRoutes } from '~/utils/constants';
33

4+
export const SUBMISSION_ACCESS_TOKEN_STORAGE_PREFIX = 'submissionAccessToken:';
5+
6+
export function getValidSubmissionAccessToken(submissionId) {
7+
if (!submissionId) return null;
8+
const token = sessionStorage.getItem(
9+
SUBMISSION_ACCESS_TOKEN_STORAGE_PREFIX + submissionId
10+
);
11+
if (!token) return null;
12+
const parts = token.split('.');
13+
if (parts.length !== 3) return null;
14+
const exp = Number(parts[1]);
15+
if (!Number.isFinite(exp) || exp <= Date.now()) return null;
16+
return token;
17+
}
18+
19+
export function clearSubmissionAccessToken(submissionId) {
20+
if (!submissionId) return;
21+
sessionStorage.removeItem(
22+
SUBMISSION_ACCESS_TOKEN_STORAGE_PREFIX + submissionId
23+
);
24+
}
25+
426
export default {
527
//
628
// Form calls
@@ -262,11 +284,19 @@ export default {
262284
* @param {Object} requestBody The form data for the submission
263285
* @returns {Promise} An axios response
264286
*/
265-
createSubmission(formId, versionId, requestBody) {
266-
return appAxios().post(
287+
async createSubmission(formId, versionId, requestBody) {
288+
const response = await appAxios().post(
267289
`${ApiRoutes.FORMS}/${formId}/versions/${versionId}/submissions`,
268290
requestBody
269291
);
292+
if (response.data?._accessToken && response.data?.id) {
293+
sessionStorage.setItem(
294+
SUBMISSION_ACCESS_TOKEN_STORAGE_PREFIX + response.data.id,
295+
response.data._accessToken
296+
);
297+
delete response.data._accessToken;
298+
}
299+
return response;
270300
},
271301

272302
/**

app/frontend/src/services/interceptors.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function appAxios(timeout = 60000) {
2929
}
3030

3131
// Add tenant ID header if tenant is selected
32-
// EXCLUDE all requests from public/submitter routes these pages operate
32+
// EXCLUDE all requests from public/submitter routes; these pages operate
3333
// outside tenant scope (form submission, submission viewing, draft editing)
3434
const noTenantHeaderPaths = [
3535
'/form/submit',
@@ -45,6 +45,19 @@ export function appAxios(timeout = 60000) {
4545
if (tenantStore?.selectedTenant?.id && !isNoTenantRoute) {
4646
cfg.headers['x-tenant-id'] = tenantStore.selectedTenant.id;
4747
}
48+
49+
const submissionIdMatch = cfg.url?.match(
50+
/\/submissions\/([0-9a-f-]{36})(?:\/|\?|$)/i
51+
);
52+
if (submissionIdMatch) {
53+
const token = sessionStorage.getItem(
54+
'submissionAccessToken:' + submissionIdMatch[1]
55+
);
56+
if (token) {
57+
cfg.headers['X-Submission-Token'] = token;
58+
}
59+
}
60+
4861
return Promise.resolve(cfg);
4962
},
5063
(error) => {

0 commit comments

Comments
 (0)