@frigear-nu/forms 0.1.1 → 0.1.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/dist/module.d.mts +18 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +29 -26
- package/dist/runtime/app/components/FormBuilderAccess.vue +255 -262
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +8 -1
- package/dist/runtime/app/components/FormBuilderSettings.vue +143 -141
- package/dist/runtime/app/components/FormCreateSlideover.vue +2 -2
- package/dist/runtime/app/components/FormEmbed.vue +2 -2
- package/dist/runtime/app/components/FormImportSlideover.d.vue.ts +8 -0
- package/dist/runtime/app/components/FormImportSlideover.vue +57 -0
- package/dist/runtime/app/components/FormImportSlideover.vue.d.ts +8 -0
- package/dist/runtime/app/components/FormRenderer.vue +3 -3
- package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
- package/dist/runtime/app/composables/useCaptchaSecrets.js +17 -7
- package/dist/runtime/app/composables/useFormActions.d.ts +14 -2
- package/dist/runtime/app/composables/useFormActions.js +33 -21
- package/dist/runtime/app/composables/useForms.d.ts +12 -5
- package/dist/runtime/app/composables/useForms.js +11 -5
- package/dist/runtime/app/pages/admin-form.vue +36 -20
- package/dist/runtime/app/pages/admin-submission.vue +27 -16
- package/dist/runtime/app/pages/admin-submissions.vue +30 -22
- package/dist/runtime/app/pages/admin.vue +29 -78
- package/dist/runtime/app/pages/form.vue +2 -1
- package/dist/runtime/i18n/lang/da.json +10 -2
- package/dist/runtime/i18n/lang/en.json +10 -2
- package/dist/runtime/server/mcp/tools/admin/get-submission.js +4 -4
- package/dist/runtime/server/mcp/tools/submit-form.js +2 -1
- package/dist/runtime/server/routes/admin/form-clone.post.js +6 -6
- package/dist/runtime/server/routes/admin/form-password.delete.js +5 -3
- package/dist/runtime/server/routes/admin/form-password.get.js +4 -2
- package/dist/runtime/server/routes/admin/form-password.put.js +4 -2
- package/dist/runtime/server/routes/admin/form-secret.delete.js +6 -5
- package/dist/runtime/server/routes/admin/form-secret.put.js +11 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.js +4 -2
- package/dist/runtime/server/routes/admin/form-token.delete.js +9 -6
- package/dist/runtime/server/routes/admin/form-tokens.get.js +5 -3
- package/dist/runtime/server/routes/admin/form-tokens.post.js +5 -4
- package/dist/runtime/server/routes/admin/form.delete.js +6 -4
- package/dist/runtime/server/routes/admin/form.get.js +4 -2
- package/dist/runtime/server/routes/admin/form.patch.js +3 -3
- package/dist/runtime/server/routes/admin/forms.get.js +37 -16
- package/dist/runtime/server/routes/admin/forms.post.js +3 -4
- package/dist/runtime/server/routes/admin/import.post.js +3 -2
- package/dist/runtime/server/routes/admin/submission-file.get.js +8 -5
- package/dist/runtime/server/routes/admin/submission-redeliver.post.js +4 -3
- package/dist/runtime/server/routes/admin/submission.get.js +5 -17
- package/dist/runtime/server/routes/admin/submissions.get.js +29 -13
- package/dist/runtime/server/routes/public/access.get.js +3 -1
- package/dist/runtime/server/routes/public/form.get.js +3 -1
- package/dist/runtime/server/routes/public/submit.post.js +2 -1
- package/dist/runtime/server/utils/access.js +3 -2
- package/dist/runtime/server/utils/admin-schemas.d.ts +9 -0
- package/dist/runtime/server/utils/admin-schemas.js +4 -0
- package/dist/runtime/server/utils/auth.d.ts +8 -4
- package/dist/runtime/server/utils/auth.js +6 -7
- package/dist/runtime/server/utils/captcha.js +3 -2
- package/dist/runtime/server/utils/forms.d.ts +12 -2
- package/dist/runtime/server/utils/forms.js +57 -10
- package/dist/runtime/server/utils/mcp-access.js +2 -1
- package/dist/runtime/server/utils/rate-limit.js +3 -2
- package/dist/runtime/server/utils/submit.d.ts +3 -0
- package/dist/runtime/server/utils/submit.js +21 -17
- package/dist/runtime/utils/api.d.ts +36 -0
- package/dist/runtime/utils/api.js +39 -0
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +17 -12
- package/dist/runtime/server/routes/admin/form-secrets.put.js +0 -12
- /package/dist/runtime/server/routes/admin/{form-secrets.put.d.ts → form-secret.put.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { resolveAccess, setAccess } from "../../utils";
|
|
2
|
+
import { formsApi, resolveAccess, setAccess } from "../../utils";
|
|
3
3
|
const definition = defineModel({ type: Object, ...{ required: true } });
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
formId: { type: String, required: true },
|
|
@@ -10,7 +10,6 @@ const toast = useToast();
|
|
|
10
10
|
const showError = useErrorToast();
|
|
11
11
|
const { copy: copyText } = useCopyToClipboard();
|
|
12
12
|
const format = useFormat();
|
|
13
|
-
const base = computed(() => `/api/forms/admin/forms/${props.formId}`);
|
|
14
13
|
const access = computed(() => resolveAccess(definition.value));
|
|
15
14
|
const availabilities = computed(() => [
|
|
16
15
|
{ value: "web", label: t("access.availabilityWeb", "Web page and API") },
|
|
@@ -50,14 +49,14 @@ function patchMoment(side, patch) {
|
|
|
50
49
|
if (schedule.value.type === "yearly") setSchedule({ ...schedule.value, [side]: { ...schedule.value[side], ...patch } });
|
|
51
50
|
}
|
|
52
51
|
const fail = (cause) => showError(cause, t("error.generic", "Something went wrong. Please try again."));
|
|
53
|
-
const { data: tokens, refresh: refreshTokens } = await useFetch(() =>
|
|
52
|
+
const { data: tokens, refresh: refreshTokens } = await useFetch(() => formsApi.admin.tokens(props.formId), { default: () => [] });
|
|
54
53
|
const tokenName = ref("");
|
|
55
54
|
const creating = ref(false);
|
|
56
55
|
const revealed = ref();
|
|
57
56
|
async function createToken() {
|
|
58
57
|
creating.value = true;
|
|
59
58
|
try {
|
|
60
|
-
revealed.value = await $fetch(
|
|
59
|
+
revealed.value = await $fetch(formsApi.admin.tokens(props.formId), { method: "POST", body: { name: tokenName.value } });
|
|
61
60
|
tokenName.value = "";
|
|
62
61
|
await refreshTokens();
|
|
63
62
|
} catch (cause) {
|
|
@@ -68,20 +67,24 @@ async function createToken() {
|
|
|
68
67
|
}
|
|
69
68
|
async function revoke(token) {
|
|
70
69
|
try {
|
|
71
|
-
await $fetch(
|
|
70
|
+
await $fetch(formsApi.admin.token(props.formId, token.id), { method: "DELETE" });
|
|
72
71
|
await refreshTokens();
|
|
73
72
|
} catch (cause) {
|
|
74
73
|
fail(cause);
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
const copy = (text) => copyText(text, t("preview.copied", "Copied to clipboard."));
|
|
78
|
-
const { data: passwordStatus, refresh: refreshPassword } = await useFetch(() =>
|
|
77
|
+
const { data: passwordStatus, refresh: refreshPassword } = await useFetch(() => formsApi.admin.password(props.formId), { default: () => ({ set: false }) });
|
|
79
78
|
const password = ref("");
|
|
80
79
|
const savingPassword = ref(false);
|
|
80
|
+
const passwordHint = computed(() => [
|
|
81
|
+
access.value.auth === "password" && !passwordStatus.value?.set ? "" : passwordStatus.value?.set ? t("access.passwordIsSet", "A password is set.") : t("access.passwordIsNotSet", "No password is set."),
|
|
82
|
+
access.value.auth !== "password" ? t("access.passwordInactive", "It is only checked when authentication is set to Shared password.") : ""
|
|
83
|
+
].filter(Boolean).join(" ") || void 0);
|
|
81
84
|
async function savePassword() {
|
|
82
85
|
savingPassword.value = true;
|
|
83
86
|
try {
|
|
84
|
-
await $fetch(
|
|
87
|
+
await $fetch(formsApi.admin.password(props.formId), { method: "PUT", body: { password: password.value } });
|
|
85
88
|
password.value = "";
|
|
86
89
|
await refreshPassword();
|
|
87
90
|
toast.add({ title: t("access.passwordSaved", "Password saved."), color: "success" });
|
|
@@ -93,14 +96,14 @@ async function savePassword() {
|
|
|
93
96
|
}
|
|
94
97
|
async function clearPassword() {
|
|
95
98
|
try {
|
|
96
|
-
await $fetch(
|
|
99
|
+
await $fetch(formsApi.admin.password(props.formId), { method: "DELETE" });
|
|
97
100
|
await refreshPassword();
|
|
98
101
|
} catch (cause) {
|
|
99
102
|
fail(cause);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
const origin = useRequestURL().origin;
|
|
103
|
-
const submitUrl = computed(() => `${origin}
|
|
106
|
+
const submitUrl = computed(() => `${origin}${formsApi.submit(props.slug)}`);
|
|
104
107
|
const snippet = computed(() => {
|
|
105
108
|
const headers = {
|
|
106
109
|
token: " -H 'Authorization: Bearer TOKEN' \\\n",
|
|
@@ -115,283 +118,273 @@ ${headers} -H 'content-type: application/json' \\
|
|
|
115
118
|
</script>
|
|
116
119
|
|
|
117
120
|
<template>
|
|
118
|
-
<div class="
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/>
|
|
134
|
-
</UFormField>
|
|
135
|
-
|
|
136
|
-
<UFormField
|
|
137
|
-
:label="t('access.auth', 'Authentication')"
|
|
138
|
-
:description="access.auth === 'token' ? t('access.authTokenHint', 'Token forms are API only: a token cannot be typed into a page.') : void 0"
|
|
139
|
-
>
|
|
140
|
-
<USelect
|
|
141
|
-
:model-value="access.auth"
|
|
142
|
-
:items="authModes"
|
|
143
|
-
class="w-full"
|
|
144
|
-
@update:model-value="definition = setAccess(definition, { auth: $event })"
|
|
145
|
-
/>
|
|
146
|
-
</UFormField>
|
|
147
|
-
<UFormField
|
|
148
|
-
:label="t('schedule.title', 'Open period')"
|
|
149
|
-
:description="t('schedule.hint', 'Outside the period the form shows a closed notice and rejects submissions. A season whose end is before its start wraps over new year.')"
|
|
150
|
-
>
|
|
151
|
-
<USelect
|
|
152
|
-
:model-value="schedule.type"
|
|
153
|
-
:items="scheduleTypes"
|
|
154
|
-
class="w-full"
|
|
155
|
-
@update:model-value="changeScheduleType($event)"
|
|
156
|
-
/>
|
|
157
|
-
</UFormField>
|
|
121
|
+
<div class="flex flex-col gap-4">
|
|
122
|
+
<SectionCard :title="t('access.title', 'Who can use this form')">
|
|
123
|
+
<div class="flex flex-col divide-y divide-default">
|
|
124
|
+
<FormRow
|
|
125
|
+
:label="t('access.availability', 'Availability')"
|
|
126
|
+
:description="access.availability === 'api' ? t('access.availabilityApiHint', 'No public page: the form can only be read and submitted through the API.') : t('access.availabilityWebHint', 'The public page and the API are both available.')"
|
|
127
|
+
>
|
|
128
|
+
<USelect
|
|
129
|
+
:model-value="access.availability"
|
|
130
|
+
:items="availabilities"
|
|
131
|
+
:disabled="access.auth === 'token'"
|
|
132
|
+
class="w-full sm:max-w-sm"
|
|
133
|
+
@update:model-value="definition = setAccess(definition, { availability: $event })"
|
|
134
|
+
/>
|
|
135
|
+
</FormRow>
|
|
158
136
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class="w-full"
|
|
167
|
-
:model-value="
|
|
168
|
-
@update:model-value="setSchedule({ ...schedule, from: fromLocalInput($event) })"
|
|
137
|
+
<FormRow
|
|
138
|
+
:label="t('access.auth', 'Authentication')"
|
|
139
|
+
:description="access.auth === 'token' ? t('access.authTokenHint', 'Token forms are API only: a token cannot be typed into a page.') : void 0"
|
|
140
|
+
>
|
|
141
|
+
<USelect
|
|
142
|
+
:model-value="access.auth"
|
|
143
|
+
:items="authModes"
|
|
144
|
+
class="w-full sm:max-w-sm"
|
|
145
|
+
@update:model-value="definition = setAccess(definition, { auth: $event })"
|
|
169
146
|
/>
|
|
170
|
-
</
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
147
|
+
</FormRow>
|
|
148
|
+
<FormRow
|
|
149
|
+
:label="t('schedule.title', 'Open period')"
|
|
150
|
+
:description="t('schedule.hint', 'Outside the period the form shows a closed notice and rejects submissions. A season whose end is before its start wraps over new year.')"
|
|
151
|
+
>
|
|
152
|
+
<USelect
|
|
153
|
+
:model-value="schedule.type"
|
|
154
|
+
:items="scheduleTypes"
|
|
155
|
+
class="w-full sm:max-w-sm"
|
|
156
|
+
@update:model-value="changeScheduleType($event)"
|
|
177
157
|
/>
|
|
178
|
-
</
|
|
179
|
-
</div>
|
|
158
|
+
</FormRow>
|
|
180
159
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class="grid gap-4 sm:grid-cols-3"
|
|
189
|
-
>
|
|
190
|
-
<UFormField :label="side === 'from' ? t('schedule.opens', 'Opens') : t('schedule.closes', 'Closes')">
|
|
191
|
-
<USelect
|
|
192
|
-
:model-value="schedule[side].month"
|
|
193
|
-
:items="months"
|
|
194
|
-
class="w-full"
|
|
195
|
-
@update:model-value="patchMoment(side, { month: Number($event) })"
|
|
160
|
+
<template v-if="schedule.type === 'range'">
|
|
161
|
+
<FormRow :label="t('schedule.opens', 'Opens')">
|
|
162
|
+
<UInput
|
|
163
|
+
type="datetime-local"
|
|
164
|
+
class="w-full sm:max-w-sm"
|
|
165
|
+
:model-value="toLocalInput(schedule.from)"
|
|
166
|
+
@update:model-value="setSchedule({ ...schedule, from: fromLocalInput($event) })"
|
|
196
167
|
/>
|
|
197
|
-
</
|
|
198
|
-
<
|
|
168
|
+
</FormRow>
|
|
169
|
+
<FormRow :label="t('schedule.closes', 'Closes')">
|
|
199
170
|
<UInput
|
|
200
|
-
type="
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
:model-value="schedule[side].day"
|
|
205
|
-
@update:model-value="patchMoment(side, { day: Number($event) })"
|
|
171
|
+
type="datetime-local"
|
|
172
|
+
class="w-full sm:max-w-sm"
|
|
173
|
+
:model-value="toLocalInput(schedule.to)"
|
|
174
|
+
@update:model-value="setSchedule({ ...schedule, to: fromLocalInput($event) })"
|
|
206
175
|
/>
|
|
207
|
-
</
|
|
208
|
-
|
|
176
|
+
</FormRow>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<template v-else-if="schedule.type === 'yearly'">
|
|
180
|
+
<FormRow
|
|
181
|
+
v-for="side in ['from', 'to']"
|
|
182
|
+
:key="side"
|
|
183
|
+
:label="side === 'from' ? t('schedule.opens', 'Opens') : t('schedule.closes', 'Closes')"
|
|
184
|
+
>
|
|
185
|
+
<div class="grid grid-cols-3 gap-2 sm:max-w-sm">
|
|
186
|
+
<UFormField :label="t('schedule.month', 'Month')">
|
|
187
|
+
<USelect
|
|
188
|
+
:model-value="schedule[side].month"
|
|
189
|
+
:items="months"
|
|
190
|
+
class="w-full"
|
|
191
|
+
@update:model-value="patchMoment(side, { month: Number($event) })"
|
|
192
|
+
/>
|
|
193
|
+
</UFormField>
|
|
194
|
+
<UFormField :label="t('schedule.day', 'Day')">
|
|
195
|
+
<UInput
|
|
196
|
+
type="number"
|
|
197
|
+
min="1"
|
|
198
|
+
max="31"
|
|
199
|
+
class="w-full"
|
|
200
|
+
:model-value="schedule[side].day"
|
|
201
|
+
@update:model-value="patchMoment(side, { day: Number($event) })"
|
|
202
|
+
/>
|
|
203
|
+
</UFormField>
|
|
204
|
+
<UFormField :label="t('schedule.time', 'Time')">
|
|
205
|
+
<UInput
|
|
206
|
+
type="time"
|
|
207
|
+
class="w-full"
|
|
208
|
+
:model-value="schedule[side].time"
|
|
209
|
+
@update:model-value="patchMoment(side, { time: String($event) })"
|
|
210
|
+
/>
|
|
211
|
+
</UFormField>
|
|
212
|
+
</div>
|
|
213
|
+
</FormRow>
|
|
214
|
+
<FormRow :label="t('schedule.timezone', 'Timezone')">
|
|
209
215
|
<UInput
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
:model-value="schedule
|
|
213
|
-
@update:model-value="patchMoment(side, { time: String($event) })"
|
|
216
|
+
class="w-full sm:max-w-sm"
|
|
217
|
+
:model-value="schedule.timezone"
|
|
218
|
+
@update:model-value="setSchedule({ ...schedule, timezone: String($event) })"
|
|
214
219
|
/>
|
|
215
|
-
</
|
|
216
|
-
</
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@update:model-value="setSchedule({ ...schedule, timezone: String($event) })"
|
|
222
|
-
/>
|
|
223
|
-
</UFormField>
|
|
220
|
+
</FormRow>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
223
|
+
<p class="pt-4 text-sm text-muted">
|
|
224
|
+
{{ t("access.saveHint", "Availability and authentication are saved with the form. Tokens and the password take effect immediately.") }}
|
|
225
|
+
</p>
|
|
224
226
|
</div>
|
|
227
|
+
</SectionCard>
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
<SectionCard :title="t('access.tokensTitle', 'API tokens')">
|
|
230
|
+
<div class="flex flex-col gap-4">
|
|
231
|
+
<p class="text-sm text-muted">
|
|
232
|
+
{{ t("access.tokensHint", "Send a token as Authorization: Bearer TOKEN or in the x-form-token header. It is shown only once.") }}
|
|
233
|
+
</p>
|
|
234
|
+
<UAlert
|
|
235
|
+
v-if="access.auth !== 'token'"
|
|
236
|
+
color="neutral"
|
|
237
|
+
variant="subtle"
|
|
238
|
+
:title="t('access.tokensInactive', 'Tokens are only checked when authentication is set to API token.')"
|
|
239
|
+
/>
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
<form
|
|
242
|
+
class="flex gap-2"
|
|
243
|
+
@submit.prevent="createToken"
|
|
244
|
+
>
|
|
245
|
+
<UInput
|
|
246
|
+
v-model="tokenName"
|
|
247
|
+
:placeholder="t('access.tokenName', 'Token name, e.g. Mobile app')"
|
|
248
|
+
class="flex-1 sm:max-w-sm"
|
|
249
|
+
/>
|
|
250
|
+
<UButton
|
|
251
|
+
type="submit"
|
|
252
|
+
:loading="creating"
|
|
253
|
+
:disabled="!tokenName.trim()"
|
|
254
|
+
:label="t('access.createToken', 'Create token')"
|
|
255
|
+
/>
|
|
256
|
+
</form>
|
|
244
257
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
<UAlert
|
|
259
|
+
v-if="revealed"
|
|
260
|
+
color="success"
|
|
261
|
+
variant="subtle"
|
|
262
|
+
:title="t('access.tokenCreated', 'Copy this token now. You will not see it again.')"
|
|
263
|
+
:close="true"
|
|
264
|
+
@update:open="revealed = void 0"
|
|
265
|
+
>
|
|
266
|
+
<template #description>
|
|
267
|
+
<div class="mt-2 flex items-center gap-2">
|
|
268
|
+
<code
|
|
269
|
+
data-testid="revealed-token"
|
|
270
|
+
class="break-all rounded bg-elevated px-2 py-1 text-xs"
|
|
271
|
+
>{{ revealed.token }}</code>
|
|
272
|
+
<UButton
|
|
273
|
+
color="neutral"
|
|
274
|
+
variant="outline"
|
|
275
|
+
size="xs"
|
|
276
|
+
icon="i-lucide-copy"
|
|
277
|
+
:label="t('preview.copy', 'Copy')"
|
|
278
|
+
@click="copy(revealed.token)"
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
</template>
|
|
282
|
+
</UAlert>
|
|
261
283
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
284
|
+
<ul
|
|
285
|
+
v-if="tokens?.length"
|
|
286
|
+
class="divide-y divide-default rounded-md border border-default"
|
|
287
|
+
>
|
|
288
|
+
<li
|
|
289
|
+
v-for="token in tokens"
|
|
290
|
+
:key="token.id"
|
|
291
|
+
class="flex items-center justify-between gap-3 p-3"
|
|
292
|
+
>
|
|
293
|
+
<div class="min-w-0">
|
|
294
|
+
<p class="truncate font-medium">
|
|
295
|
+
{{ token.name }}
|
|
296
|
+
<StatusBadge
|
|
297
|
+
v-if="token.revokedAt"
|
|
298
|
+
status="revoked"
|
|
299
|
+
size="sm"
|
|
300
|
+
:label="t('access.revoked', 'Revoked')"
|
|
301
|
+
:colors="{ revoked: 'error' }"
|
|
302
|
+
/>
|
|
303
|
+
</p>
|
|
304
|
+
<p class="text-xs text-muted">
|
|
305
|
+
<code>{{ token.prefix }}…</code>
|
|
306
|
+
·
|
|
307
|
+
{{ token.lastUsedAt ? t("access.lastUsed", "Last used {date}", { date: format.dateTime(token.lastUsedAt) }) : t("access.neverUsed", "Never used") }}
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
276
310
|
<UButton
|
|
277
|
-
|
|
311
|
+
v-if="!token.revokedAt"
|
|
312
|
+
color="error"
|
|
278
313
|
variant="outline"
|
|
279
314
|
size="xs"
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
@click="copy(revealed.token)"
|
|
315
|
+
:label="t('access.revoke', 'Revoke')"
|
|
316
|
+
@click="revoke(token)"
|
|
283
317
|
/>
|
|
284
|
-
</
|
|
285
|
-
</
|
|
286
|
-
|
|
318
|
+
</li>
|
|
319
|
+
</ul>
|
|
320
|
+
<p
|
|
321
|
+
v-else
|
|
322
|
+
class="text-sm text-muted"
|
|
323
|
+
>
|
|
324
|
+
{{ t("access.noTokens", "No tokens yet.") }}
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
</SectionCard>
|
|
287
328
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
:
|
|
295
|
-
|
|
329
|
+
<SectionCard :title="t('access.passwordTitle', 'Shared password')">
|
|
330
|
+
<div class="flex flex-col gap-4">
|
|
331
|
+
<UAlert
|
|
332
|
+
v-if="access.auth === 'password' && !passwordStatus?.set"
|
|
333
|
+
color="warning"
|
|
334
|
+
variant="subtle"
|
|
335
|
+
:title="t('access.passwordMissing', 'No password is set, so nobody can open this form yet.')"
|
|
336
|
+
/>
|
|
337
|
+
<FormRow
|
|
338
|
+
:label="t('access.passwordLabel', 'Password')"
|
|
339
|
+
:description="passwordHint"
|
|
296
340
|
>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
341
|
+
<form
|
|
342
|
+
class="flex flex-wrap gap-2"
|
|
343
|
+
@submit.prevent="savePassword"
|
|
344
|
+
>
|
|
345
|
+
<UInput
|
|
346
|
+
v-model="password"
|
|
347
|
+
type="password"
|
|
348
|
+
autocomplete="new-password"
|
|
349
|
+
:placeholder="passwordStatus?.set ? t('access.newPassword', 'New password') : t('access.setPassword', 'Password (min. 6 characters)')"
|
|
350
|
+
class="min-w-0 flex-1 sm:max-w-sm"
|
|
351
|
+
/>
|
|
352
|
+
<UButton
|
|
353
|
+
type="submit"
|
|
354
|
+
:loading="savingPassword"
|
|
355
|
+
:disabled="password.length < 6"
|
|
356
|
+
:label="passwordStatus?.set ? t('access.changePassword', 'Change') : t('access.savePassword', 'Set password')"
|
|
357
|
+
/>
|
|
358
|
+
<UButton
|
|
359
|
+
v-if="passwordStatus?.set"
|
|
360
|
+
type="button"
|
|
361
|
+
color="error"
|
|
362
|
+
variant="outline"
|
|
363
|
+
:label="t('access.clearPassword', 'Clear')"
|
|
364
|
+
@click="clearPassword"
|
|
365
|
+
/>
|
|
366
|
+
</form>
|
|
367
|
+
</FormRow>
|
|
368
|
+
</div>
|
|
369
|
+
</SectionCard>
|
|
370
|
+
|
|
371
|
+
<SectionCard :title="t('access.apiTitle', 'API')">
|
|
372
|
+
<div class="flex flex-col gap-4">
|
|
373
|
+
<p class="text-sm text-muted">
|
|
374
|
+
{{ t("access.apiHint", "Submit with POST (JSON, or multipart with file:KEY parts). The definition is available with GET on /api/forms/{slug}.", { slug: props.slug }) }}
|
|
375
|
+
</p>
|
|
376
|
+
<div class="flex items-start gap-2">
|
|
377
|
+
<pre class="flex-1 overflow-x-auto rounded-md bg-elevated p-3 text-xs">{{ snippet }}</pre>
|
|
314
378
|
<UButton
|
|
315
|
-
|
|
316
|
-
color="error"
|
|
379
|
+
color="neutral"
|
|
317
380
|
variant="outline"
|
|
318
381
|
size="xs"
|
|
319
|
-
|
|
320
|
-
|
|
382
|
+
icon="i-lucide-copy"
|
|
383
|
+
:label="t('preview.copy', 'Copy')"
|
|
384
|
+
@click="copy(snippet)"
|
|
321
385
|
/>
|
|
322
|
-
</
|
|
323
|
-
</ul>
|
|
324
|
-
<p
|
|
325
|
-
v-else
|
|
326
|
-
class="text-sm text-muted"
|
|
327
|
-
>
|
|
328
|
-
{{ t("access.noTokens", "No tokens yet.") }}
|
|
329
|
-
</p>
|
|
330
|
-
</section>
|
|
331
|
-
|
|
332
|
-
<section class="space-y-4">
|
|
333
|
-
<h2 class="font-semibold">
|
|
334
|
-
{{ t("access.passwordTitle", "Shared password") }}
|
|
335
|
-
</h2>
|
|
336
|
-
<UAlert
|
|
337
|
-
v-if="access.auth === 'password' && !passwordStatus?.set"
|
|
338
|
-
color="warning"
|
|
339
|
-
variant="subtle"
|
|
340
|
-
:title="t('access.passwordMissing', 'No password is set, so nobody can open this form yet.')"
|
|
341
|
-
/>
|
|
342
|
-
<p
|
|
343
|
-
v-else
|
|
344
|
-
class="text-sm text-muted"
|
|
345
|
-
>
|
|
346
|
-
{{ passwordStatus?.set ? t("access.passwordIsSet", "A password is set.") : t("access.passwordIsNotSet", "No password is set.") }}
|
|
347
|
-
{{ access.auth !== "password" ? t("access.passwordInactive", "It is only checked when authentication is set to Shared password.") : "" }}
|
|
348
|
-
</p>
|
|
349
|
-
<form
|
|
350
|
-
class="flex gap-2"
|
|
351
|
-
@submit.prevent="savePassword"
|
|
352
|
-
>
|
|
353
|
-
<UInput
|
|
354
|
-
v-model="password"
|
|
355
|
-
type="password"
|
|
356
|
-
autocomplete="new-password"
|
|
357
|
-
:placeholder="passwordStatus?.set ? t('access.newPassword', 'New password') : t('access.setPassword', 'Password (min. 6 characters)')"
|
|
358
|
-
class="flex-1"
|
|
359
|
-
/>
|
|
360
|
-
<UButton
|
|
361
|
-
type="submit"
|
|
362
|
-
:loading="savingPassword"
|
|
363
|
-
:disabled="password.length < 6"
|
|
364
|
-
:label="passwordStatus?.set ? t('access.changePassword', 'Change') : t('access.savePassword', 'Set password')"
|
|
365
|
-
/>
|
|
366
|
-
<UButton
|
|
367
|
-
v-if="passwordStatus?.set"
|
|
368
|
-
type="button"
|
|
369
|
-
color="error"
|
|
370
|
-
variant="outline"
|
|
371
|
-
:label="t('access.clearPassword', 'Clear')"
|
|
372
|
-
@click="clearPassword"
|
|
373
|
-
/>
|
|
374
|
-
</form>
|
|
375
|
-
</section>
|
|
376
|
-
|
|
377
|
-
<section class="space-y-3">
|
|
378
|
-
<h2 class="font-semibold">
|
|
379
|
-
{{ t("access.apiTitle", "API") }}
|
|
380
|
-
</h2>
|
|
381
|
-
<p class="text-sm text-muted">
|
|
382
|
-
{{ t("access.apiHint", "Submit with POST (JSON, or multipart with file:KEY parts). The definition is available with GET on /api/forms/{slug}.", { slug: props.slug }) }}
|
|
383
|
-
</p>
|
|
384
|
-
<div class="flex items-start gap-2">
|
|
385
|
-
<pre class="flex-1 overflow-x-auto rounded-md bg-elevated p-3 text-xs">{{ snippet }}</pre>
|
|
386
|
-
<UButton
|
|
387
|
-
color="neutral"
|
|
388
|
-
variant="outline"
|
|
389
|
-
size="xs"
|
|
390
|
-
icon="i-lucide-copy"
|
|
391
|
-
:label="t('preview.copy', 'Copy')"
|
|
392
|
-
@click="copy(snippet)"
|
|
393
|
-
/>
|
|
386
|
+
</div>
|
|
394
387
|
</div>
|
|
395
|
-
</
|
|
388
|
+
</SectionCard>
|
|
396
389
|
</div>
|
|
397
390
|
</template>
|
|
@@ -60,7 +60,14 @@ const keyMissing = computed(() => !captchaSiteKey(props.field));
|
|
|
60
60
|
data-testid="captcha-secret-status"
|
|
61
61
|
>
|
|
62
62
|
<span class="text-muted">{{ t("captcha.secretStatus", "Secret key") }}:</span>
|
|
63
|
-
<
|
|
63
|
+
<USkeleton
|
|
64
|
+
v-if="secrets.loading"
|
|
65
|
+
class="h-5 w-32"
|
|
66
|
+
/>
|
|
67
|
+
<span
|
|
68
|
+
v-else
|
|
69
|
+
:class="secrets.source === 'unset' ? 'text-warning' : 'text-success'"
|
|
70
|
+
>{{ secrets.statusLabel }}</span>
|
|
64
71
|
</p>
|
|
65
72
|
<UAlert
|
|
66
73
|
v-if="secrets.secrets?.unreadable"
|