@frigear-nu/forms 0.0.6 → 0.0.8
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/README.md +16 -1
- package/dist/module.d.mts +148 -8
- package/dist/module.json +1 -1
- package/dist/module.mjs +65 -35
- package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
- package/dist/runtime/app/components/FormBuilder.vue +220 -0
- package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
- package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
- package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormBuilderText.vue +127 -0
- package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.vue +95 -0
- package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
- package/dist/runtime/app/components/FormEmbed.vue +113 -0
- package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
- package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
- package/dist/runtime/app/components/FormField.vue +35 -3
- package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
- package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
- package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
- package/dist/runtime/app/components/FormPreview.vue +244 -0
- package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
- package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
- package/dist/runtime/app/components/FormRenderer.vue +16 -7
- package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
- package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
- package/dist/runtime/app/composables/index.d.ts +4 -1
- package/dist/runtime/app/composables/index.js +4 -1
- package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
- package/dist/runtime/app/composables/useCaptcha.js +39 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
- package/dist/runtime/app/composables/useForms.d.ts +18 -3
- package/dist/runtime/app/composables/useForms.js +38 -5
- package/dist/runtime/app/pages/admin-form.vue +112 -69
- package/dist/runtime/app/pages/admin-submission.vue +7 -16
- package/dist/runtime/app/pages/admin-submissions.vue +41 -46
- package/dist/runtime/app/pages/admin.vue +128 -44
- package/dist/runtime/app/pages/form.vue +71 -5
- package/dist/runtime/i18n/lang/da.json +231 -3
- package/dist/runtime/i18n/lang/en.json +231 -3
- package/dist/runtime/menus.js +10 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
- package/dist/runtime/server/db/schema.sqlite.js +33 -1
- package/dist/runtime/server/mcp/schemas.d.ts +9 -0
- package/dist/runtime/server/mcp/schemas.js +8 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
- package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-form.js +12 -0
- package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
- package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
- package/dist/runtime/server/plugins/email-delivery.js +16 -11
- package/dist/runtime/server/plugins/encryption.d.ts +2 -0
- package/dist/runtime/server/plugins/encryption.js +4 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
- package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
- package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
- package/dist/runtime/server/routes/admin/form.get.js +5 -1
- package/dist/runtime/server/routes/admin/form.patch.js +3 -12
- package/dist/runtime/server/routes/admin/forms.post.js +1 -6
- package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/access.get.js +3 -0
- package/dist/runtime/server/routes/public/form.get.js +3 -1
- package/dist/runtime/server/routes/public/submit.post.js +2 -2
- package/dist/runtime/server/utils/access.d.ts +27 -0
- package/dist/runtime/server/utils/access.js +63 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
- package/dist/runtime/server/utils/auth.d.ts +7 -7
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/captcha.d.ts +10 -0
- package/dist/runtime/server/utils/captcha.js +25 -0
- package/dist/runtime/server/utils/crypto.d.ts +18 -0
- package/dist/runtime/server/utils/crypto.js +48 -0
- package/dist/runtime/server/utils/forms.d.ts +6 -1
- package/dist/runtime/server/utils/forms.js +28 -0
- package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
- package/dist/runtime/server/utils/mcp-access.js +8 -0
- package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
- package/dist/runtime/server/utils/rate-limit.js +19 -0
- package/dist/runtime/server/utils/render.d.ts +2 -11
- package/dist/runtime/server/utils/render.js +1 -26
- package/dist/runtime/server/utils/secrets.d.ts +28 -0
- package/dist/runtime/server/utils/secrets.js +60 -0
- package/dist/runtime/server/utils/submit.d.ts +12 -1
- package/dist/runtime/server/utils/submit.js +18 -4
- package/dist/runtime/utils/builder.d.ts +73 -0
- package/dist/runtime/utils/builder.js +220 -0
- package/dist/runtime/utils/captcha.d.ts +9 -0
- package/dist/runtime/utils/captcha.js +7 -0
- package/dist/runtime/utils/conditions.d.ts +3 -1
- package/dist/runtime/utils/conditions.js +4 -0
- package/dist/runtime/utils/definition.d.ts +116 -6
- package/dist/runtime/utils/definition.js +53 -4
- package/dist/runtime/utils/embed.d.ts +8 -0
- package/dist/runtime/utils/embed.js +7 -0
- package/dist/runtime/utils/index.d.ts +16 -3
- package/dist/runtime/utils/index.js +66 -3
- package/dist/runtime/utils/markdown.d.ts +1 -5
- package/dist/runtime/utils/markdown.js +1 -5
- package/dist/runtime/utils/phone.d.ts +29 -0
- package/dist/runtime/utils/phone.js +294 -0
- package/dist/runtime/utils/preview.d.ts +12 -0
- package/dist/runtime/utils/preview.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +1 -0
- package/dist/runtime/utils/schedule.js +1 -0
- package/dist/runtime/utils/schema.d.ts +2 -0
- package/dist/runtime/utils/schema.js +12 -4
- package/dist/runtime/utils/template.d.ts +57 -0
- package/dist/runtime/utils/template.js +147 -0
- package/dist/runtime/utils/text.d.ts +0 -3
- package/dist/runtime/utils/text.js +0 -5
- package/dist/types.d.mts +5 -1
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -4,7 +4,22 @@ Dynamic, database-stored forms for the frigear ecosystem. A form is a JSON defin
|
|
|
4
4
|
with conditional fields), submissions are stored with a snapshot of the definition they were answered against, and
|
|
5
5
|
delivery (email, webhooks, ...) hangs off the `forms:submitted` Nitro hook.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Captcha fields (Cloudflare Turnstile, reCAPTCHA, hCaptcha) need a site key (`NUXT_PUBLIC_FORMS_CAPTCHA_<PROVIDER>_SITE_KEY`)
|
|
8
|
+
and a secret key (`NUXT_FORMS_CAPTCHA_<PROVIDER>_SECRET_KEY`) with `<PROVIDER>` one of `TURNSTILE`, `RECAPTCHA`, `HCAPTCHA`;
|
|
9
|
+
`NUXT_PUBLIC_FORMS_CAPTCHA_DEFAULT_PROVIDER` picks the provider of a field that names none. A secret key can also be set
|
|
10
|
+
per form in the admin builder: it is stored encrypted (AES-256-GCM, key from `NUXT_FORMS_ENCRYPTION_KEY`, else
|
|
11
|
+
`NUXT_ENCRYPTION_KEY`, else the session password; changing the key makes stored secrets unreadable) and takes precedence
|
|
12
|
+
over the environment one. Without any secret key submissions fail closed.
|
|
13
|
+
|
|
14
|
+
Embed a published form in Markdown (Nuxt Content / MDC) with the globally registered `FormEmbed` component
|
|
15
|
+
(`slug`, `title`, `locale`, `class`; honors the form's access settings):
|
|
16
|
+
|
|
17
|
+
```md
|
|
18
|
+
::form-embed{slug="contact"}
|
|
19
|
+
::
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Full documentation: `apps/docs/content/4.modules/07.forms.md`.
|
|
8
23
|
|
|
9
24
|
```ts
|
|
10
25
|
export default defineNuxtConfig({
|
package/dist/module.d.mts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { H3Event } from 'h3';
|
|
3
|
+
import { Schedule } from '@frigear-nu/module-kit/schedule';
|
|
4
|
+
export { Schedule as FormSchedule, ScheduleMoment as FormScheduleMoment } from '@frigear-nu/module-kit/schedule';
|
|
5
|
+
import { Translated } from '@frigear-nu/ui/translated';
|
|
6
|
+
export { Translated } from '@frigear-nu/ui/translated';
|
|
3
7
|
|
|
4
|
-
/** A text that is either one string or a map of locale -> string. */
|
|
5
|
-
type Translated = string | Record<string, string>;
|
|
6
8
|
type FormStatus = 'draft' | 'published' | 'archived';
|
|
7
9
|
type FormMode = 'single' | 'stepped';
|
|
8
10
|
type FormStorage = 'db' | 'none';
|
|
9
11
|
type DeliveryStatus = 'none' | 'pending' | 'delivered' | 'failed';
|
|
10
|
-
type FieldType = 'text' | 'email' | 'tel' | 'number' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'date' | 'file' | 'markdown';
|
|
11
|
-
type
|
|
12
|
+
type FieldType = 'text' | 'email' | 'tel' | 'number' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'date' | 'file' | 'markdown' | 'captcha';
|
|
13
|
+
type CaptchaProvider = 'turnstile' | 'recaptcha' | 'hcaptcha';
|
|
14
|
+
type ConditionOp = 'eq' | 'neq' | 'in' | 'empty' | 'notEmpty' | 'truthy';
|
|
12
15
|
/** Shows a field (or step) only while the condition on another field's answer holds. */
|
|
13
16
|
type Condition = {
|
|
14
17
|
/** Key of the field whose answer is inspected. */
|
|
15
18
|
field: string;
|
|
16
19
|
op: ConditionOp;
|
|
17
|
-
/** Compared value for `eq`/`neq`; a list of accepted values for `in`. Unused for `truthy
|
|
20
|
+
/** Compared value for `eq`/`neq`; a list of accepted values for `in`. Unused for `empty`, `notEmpty` and `truthy` (an alias of `notEmpty`). */
|
|
18
21
|
value?: unknown;
|
|
19
22
|
};
|
|
20
23
|
type FieldOption = {
|
|
@@ -37,6 +40,24 @@ type FieldValidation = {
|
|
|
37
40
|
/** Replaces the default error message of every rule on this field. */
|
|
38
41
|
message?: Translated;
|
|
39
42
|
};
|
|
43
|
+
/** Settings of a `captcha` field. The visitor's response token lives in the form state only and is never stored. */
|
|
44
|
+
type CaptchaConfig = {
|
|
45
|
+
/** Defaults to `turnstile`. */
|
|
46
|
+
provider?: CaptchaProvider;
|
|
47
|
+
/** Public site key; overrides `runtimeConfig.public.forms.captcha.<provider>.siteKey` for this field. */
|
|
48
|
+
siteKey?: string;
|
|
49
|
+
theme?: 'auto' | 'light' | 'dark';
|
|
50
|
+
size?: 'normal' | 'compact';
|
|
51
|
+
};
|
|
52
|
+
/** Settings of a `tel` field. The answer is always one string; with a country selector it is `+<dial code><number>`, e.g. `+4512345678`. */
|
|
53
|
+
type PhoneConfig = {
|
|
54
|
+
/** `simple`: one free-form input (default). `country`: a country selector next to the national number, merged into one value. */
|
|
55
|
+
mode?: 'simple' | 'country';
|
|
56
|
+
/** ISO 3166-1 alpha-2 codes the visitor may choose from (`country` mode). Default: all countries. */
|
|
57
|
+
countries?: string[];
|
|
58
|
+
/** Preselected country (`country` mode). Defaults to `DK` when offered, else the first offered country. */
|
|
59
|
+
defaultCountry?: string;
|
|
60
|
+
};
|
|
40
61
|
type FormField = {
|
|
41
62
|
/** Unique across the whole form; the key of the answer. */
|
|
42
63
|
key: string;
|
|
@@ -50,9 +71,13 @@ type FormField = {
|
|
|
50
71
|
options?: FieldOption[];
|
|
51
72
|
/** `file` fields only: allow more than one file. */
|
|
52
73
|
multiple?: boolean;
|
|
53
|
-
/** Browser `autocomplete` token, e.g. `email`. */
|
|
74
|
+
/** Browser `autocomplete` token, e.g. `email`. Defaults by type: `email` for email, `tel` for phone (`tel-national` next to a country selector). */
|
|
54
75
|
autocomplete?: string;
|
|
55
76
|
validation?: FieldValidation;
|
|
77
|
+
/** `tel` fields only. */
|
|
78
|
+
phone?: PhoneConfig;
|
|
79
|
+
/** `captcha` fields only. */
|
|
80
|
+
captcha?: CaptchaConfig;
|
|
56
81
|
showIf?: Condition;
|
|
57
82
|
};
|
|
58
83
|
type FormStep = {
|
|
@@ -65,9 +90,22 @@ type FormStep = {
|
|
|
65
90
|
};
|
|
66
91
|
type FormDelivery = {
|
|
67
92
|
email?: {
|
|
68
|
-
/** Recipient addresses. */
|
|
93
|
+
/** Recipient addresses; used when no `routes` entry matches. */
|
|
69
94
|
to: string[];
|
|
95
|
+
/** May use `{{variables}}` (see the forms docs). */
|
|
70
96
|
subject?: Translated;
|
|
97
|
+
/**
|
|
98
|
+
* Custom plain-text body with `{{variables}}` (`{{answers}}`, `{{answer.<key>}}`, `{{form.name}}`, ...).
|
|
99
|
+
* Without it the email lists the answers.
|
|
100
|
+
*/
|
|
101
|
+
body?: Translated;
|
|
102
|
+
/** Sends to different recipients depending on the answer of one field (e.g. a subject select). */
|
|
103
|
+
routes?: {
|
|
104
|
+
field: string;
|
|
105
|
+
to: Record<string, string[]>;
|
|
106
|
+
};
|
|
107
|
+
/** Key of an email field whose answer becomes the `Reply-To` of the delivered email. */
|
|
108
|
+
replyToField?: string;
|
|
71
109
|
};
|
|
72
110
|
};
|
|
73
111
|
type FormSubmitSettings = {
|
|
@@ -80,6 +118,18 @@ type FormSubmitSettings = {
|
|
|
80
118
|
keep: string[];
|
|
81
119
|
};
|
|
82
120
|
};
|
|
121
|
+
/** `web`: public page and API. `api`: API only, the public page does not exist. */
|
|
122
|
+
type FormAvailability = 'web' | 'api';
|
|
123
|
+
/** How a submitter proves access: nothing, a signed-in user, a bearer token or a shared password. */
|
|
124
|
+
type FormAuthMode = 'none' | 'user' | 'token' | 'password';
|
|
125
|
+
|
|
126
|
+
/** Who may read and submit the form, and when. Secrets (tokens, password hash) are never part of the definition. */
|
|
127
|
+
type FormAccess = {
|
|
128
|
+
availability: FormAvailability;
|
|
129
|
+
auth: FormAuthMode;
|
|
130
|
+
/** Defaults to `{ type: 'always' }`. */
|
|
131
|
+
schedule?: Schedule;
|
|
132
|
+
};
|
|
83
133
|
type FormDefinition = {
|
|
84
134
|
mode: FormMode;
|
|
85
135
|
/** `none` never stores answers (or files): they are only emitted through `forms:submitted` and emailed. */
|
|
@@ -87,9 +137,12 @@ type FormDefinition = {
|
|
|
87
137
|
steps: FormStep[];
|
|
88
138
|
submit?: FormSubmitSettings;
|
|
89
139
|
delivery?: FormDelivery;
|
|
140
|
+
/** Defaults to `{ availability: 'web', auth: 'none' }` (open to everyone, as forms always were). */
|
|
141
|
+
access?: FormAccess;
|
|
90
142
|
};
|
|
91
143
|
type ModuleOptions = {
|
|
92
144
|
settings?: {
|
|
145
|
+
mcp?: boolean;
|
|
93
146
|
pages?: {
|
|
94
147
|
/** Public form page; must contain a `:slug` param. `true` = `/forms/:slug`. */
|
|
95
148
|
form?: string | boolean;
|
|
@@ -115,6 +168,22 @@ type ModuleOptions = {
|
|
|
115
168
|
enabled?: boolean;
|
|
116
169
|
forms?: SeedForm[];
|
|
117
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Keys for `captcha` fields. Site keys are public, secret keys stay on the server. Both can be set per
|
|
173
|
+
* provider here or through `NUXT_PUBLIC_FORMS_CAPTCHA_<PROVIDER>_SITE_KEY` / `NUXT_FORMS_CAPTCHA_<PROVIDER>_SECRET_KEY`.
|
|
174
|
+
*/
|
|
175
|
+
captcha?: Partial<Record<CaptchaProvider, {
|
|
176
|
+
siteKey?: string;
|
|
177
|
+
secretKey?: string;
|
|
178
|
+
}>> & {
|
|
179
|
+
/** Provider of a captcha field that does not name one. Defaults to `turnstile`; env `NUXT_PUBLIC_FORMS_CAPTCHA_DEFAULT_PROVIDER`. */
|
|
180
|
+
defaultProvider?: CaptchaProvider;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Key that encrypts per-form secrets in the database (env `NUXT_FORMS_ENCRYPTION_KEY`). Falls back to
|
|
184
|
+
* `NUXT_ENCRYPTION_KEY`, then to the session password. Changing it makes stored secrets unreadable.
|
|
185
|
+
*/
|
|
186
|
+
encryptionKey?: string;
|
|
118
187
|
};
|
|
119
188
|
type SeedForm = {
|
|
120
189
|
slug: string;
|
|
@@ -129,6 +198,10 @@ type FormsRuntimeConfig = {
|
|
|
129
198
|
enabled: boolean;
|
|
130
199
|
forms: SeedForm[];
|
|
131
200
|
};
|
|
201
|
+
captcha: Record<CaptchaProvider, {
|
|
202
|
+
secretKey: string;
|
|
203
|
+
}>;
|
|
204
|
+
encryptionKey: string;
|
|
132
205
|
};
|
|
133
206
|
type PublicModuleOptions = {
|
|
134
207
|
pages: {
|
|
@@ -138,6 +211,11 @@ type PublicModuleOptions = {
|
|
|
138
211
|
adminSubmissions?: string;
|
|
139
212
|
adminSubmission?: string;
|
|
140
213
|
};
|
|
214
|
+
captcha: Record<CaptchaProvider, {
|
|
215
|
+
siteKey: string;
|
|
216
|
+
}> & {
|
|
217
|
+
defaultProvider: CaptchaProvider;
|
|
218
|
+
};
|
|
141
219
|
};
|
|
142
220
|
type AuthorizeContext = {
|
|
143
221
|
event: H3Event;
|
|
@@ -172,6 +250,22 @@ type SubmittedContext = {
|
|
|
172
250
|
/** Set by a handler to report that it delivered the submission (email delivery does). */
|
|
173
251
|
delivered: boolean;
|
|
174
252
|
};
|
|
253
|
+
type CaptchaVerifyContext = {
|
|
254
|
+
event: H3Event;
|
|
255
|
+
provider: CaptchaProvider;
|
|
256
|
+
token: string;
|
|
257
|
+
ip: string | undefined;
|
|
258
|
+
/** The form being submitted, when known. */
|
|
259
|
+
formId?: string;
|
|
260
|
+
/**
|
|
261
|
+
* The secret key the siteverify call would use: the form's own encrypted secret, else the environment/config
|
|
262
|
+
* one, else `undefined` (which fails closed). Server-side only, never sent anywhere but the provider.
|
|
263
|
+
*/
|
|
264
|
+
secretKey?: string;
|
|
265
|
+
secretSource?: SecretSource;
|
|
266
|
+
/** Set by a handler to decide the outcome; left `undefined` the provider's siteverify endpoint is asked. */
|
|
267
|
+
valid: boolean | undefined;
|
|
268
|
+
};
|
|
175
269
|
type DeliveredContext = {
|
|
176
270
|
submissionId: string | null;
|
|
177
271
|
formSlug: string;
|
|
@@ -186,6 +280,45 @@ type PublicFormDto = {
|
|
|
186
280
|
/** The definition without `delivery` (recipients are never public). */
|
|
187
281
|
definition: Omit<FormDefinition, 'delivery'>;
|
|
188
282
|
};
|
|
283
|
+
type FormAccessTokenDto = {
|
|
284
|
+
id: string;
|
|
285
|
+
name: string;
|
|
286
|
+
/** First characters of the token, to recognise it; the token itself is shown once on creation. */
|
|
287
|
+
prefix: string;
|
|
288
|
+
createdAt: string;
|
|
289
|
+
lastUsedAt: string | null;
|
|
290
|
+
revokedAt: string | null;
|
|
291
|
+
};
|
|
292
|
+
type CreatedFormAccessTokenDto = FormAccessTokenDto & {
|
|
293
|
+
token: string;
|
|
294
|
+
};
|
|
295
|
+
type FormPasswordStatusDto = {
|
|
296
|
+
set: boolean;
|
|
297
|
+
};
|
|
298
|
+
/** Where a captcha secret key comes from: the form's encrypted secret, the environment/config, or nowhere. */
|
|
299
|
+
type SecretSource = 'form' | 'env' | 'unset';
|
|
300
|
+
/** Which per-form secrets are set. Names only: values are write-only and never leave the server. */
|
|
301
|
+
type FormSecretsDto = {
|
|
302
|
+
/** Names of the secrets stored for this form. */
|
|
303
|
+
keys: string[];
|
|
304
|
+
/** Per captcha provider, which secret key a submission would use. */
|
|
305
|
+
captcha: Record<CaptchaProvider, {
|
|
306
|
+
source: SecretSource;
|
|
307
|
+
}>;
|
|
308
|
+
/** The stored secrets cannot be decrypted (the encryption key changed); set them again. */
|
|
309
|
+
unreadable: boolean;
|
|
310
|
+
};
|
|
311
|
+
/** What the public page needs to decide between the form, a gate and "not found". Never contains secrets. */
|
|
312
|
+
type FormAccessInfoDto = {
|
|
313
|
+
slug: string;
|
|
314
|
+
name: string;
|
|
315
|
+
access: FormAccess;
|
|
316
|
+
signedIn: boolean;
|
|
317
|
+
/** False outside the schedule: the page shows a "closed" notice instead of the form. */
|
|
318
|
+
open: boolean;
|
|
319
|
+
/** Next moment the form opens (ISO), when it is closed and will open again. */
|
|
320
|
+
opensAt?: string | null;
|
|
321
|
+
};
|
|
189
322
|
type FormSummaryDto = {
|
|
190
323
|
id: string;
|
|
191
324
|
slug: string;
|
|
@@ -204,6 +337,8 @@ type FormAdminDto = {
|
|
|
204
337
|
status: FormStatus;
|
|
205
338
|
version: number;
|
|
206
339
|
definition: FormDefinition;
|
|
340
|
+
/** Stored submissions; only set when the form is fetched on its own. Can be above 0 even when storage is now `none`. */
|
|
341
|
+
submissionCount?: number;
|
|
207
342
|
createdAt: string;
|
|
208
343
|
updatedAt: string;
|
|
209
344
|
};
|
|
@@ -271,6 +406,11 @@ declare module 'nitropack' {
|
|
|
271
406
|
* here (webhooks, CRM, ...); set `ctx.delivered = true` once done. Throwing marks delivery failed.
|
|
272
407
|
*/
|
|
273
408
|
'forms:submitted': (ctx: SubmittedContext) => void | Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* A captcha token must be verified. Set `ctx.valid` to decide; when left `undefined` the provider's
|
|
411
|
+
* siteverify endpoint is called with the configured secret key. Lets hosts and tests replace the network call.
|
|
412
|
+
*/
|
|
413
|
+
'forms:captcha:verify': (ctx: CaptchaVerifyContext) => void | Promise<void>;
|
|
274
414
|
/** Delivery of a submission finished (successfully or not). */
|
|
275
415
|
'forms:delivered': (ctx: DeliveredContext) => void | Promise<void>;
|
|
276
416
|
}
|
|
@@ -278,4 +418,4 @@ declare module 'nitropack' {
|
|
|
278
418
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
279
419
|
|
|
280
420
|
export { _default as default };
|
|
281
|
-
export type { AuthorizeContext, Condition, ConditionOp, DeliveredContext, DeliveryStatus, FieldOption, FieldType, FieldValidation, FileDescriptor, FormAdminDto, FormDefinition, FormDelivery, FormField, FormMode, FormStatus, FormStep, FormStorage, FormSubmitSettings, FormSummaryDto, FormsRuntimeConfig, ModuleOptions, PublicFormDto, PublicModuleOptions, SeedForm, SubmissionDto, SubmissionFileDto, SubmissionSummaryDto, SubmitResponse, SubmittedContext, SubmittedFile
|
|
421
|
+
export type { AuthorizeContext, CaptchaConfig, CaptchaProvider, CaptchaVerifyContext, Condition, ConditionOp, CreatedFormAccessTokenDto, DeliveredContext, DeliveryStatus, FieldOption, FieldType, FieldValidation, FileDescriptor, FormAccess, FormAccessInfoDto, FormAccessTokenDto, FormAdminDto, FormAuthMode, FormAvailability, FormDefinition, FormDelivery, FormField, FormMode, FormPasswordStatusDto, FormSecretsDto, FormStatus, FormStep, FormStorage, FormSubmitSettings, FormSummaryDto, FormsRuntimeConfig, ModuleOptions, PhoneConfig, PublicFormDto, PublicModuleOptions, SecretSource, SeedForm, SubmissionDto, SubmissionFileDto, SubmissionSummaryDto, SubmitResponse, SubmittedContext, SubmittedFile };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports, addComponentsDir,
|
|
2
|
-
import { registerModule, registerLocales, registerDatabaseTable,
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, addComponent, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, inlineModuleRuntime, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
3
|
+
import { adminRoute } from '@frigear-nu/admin/utils';
|
|
4
|
+
import { addAuthScope } from '@frigear-nu/auth/utils';
|
|
5
|
+
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
3
6
|
|
|
4
7
|
const MODULE_NAME = "@frigear-nu/forms";
|
|
5
|
-
function resolvePagePath(setting, defaultPath) {
|
|
6
|
-
if (typeof setting === "string") return setting;
|
|
7
|
-
if (setting === true) return defaultPath;
|
|
8
|
-
return void 0;
|
|
9
|
-
}
|
|
10
8
|
const module$1 = defineNuxtModule({
|
|
11
9
|
meta: { name: MODULE_NAME, configKey: "forms" },
|
|
12
10
|
defaults: {
|
|
@@ -19,25 +17,44 @@ const module$1 = defineNuxtModule({
|
|
|
19
17
|
defaults: { kv: true, blob: true }
|
|
20
18
|
},
|
|
21
19
|
"@frigear-nu/module-kit": { version: ">=0.0.1" },
|
|
20
|
+
"@frigear-nu/auth": { version: ">=0.0.1" },
|
|
21
|
+
"@frigear-nu/mcp": { version: ">=0.0.1" },
|
|
22
22
|
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
23
23
|
"@frigear-nu/notify": { version: ">=0.0.1" },
|
|
24
|
-
"@frigear-nu/
|
|
24
|
+
"@frigear-nu/ui": { version: ">=0.0.1" },
|
|
25
|
+
"@frigear-nu/menu": { version: ">=0.0.1", optional: true },
|
|
26
|
+
// Provides the shared page components of the admin pages (`AdminPage`, `AdminTable`, ...).
|
|
27
|
+
"@frigear-nu/admin": { version: ">=0.0.0", optional: true }
|
|
25
28
|
},
|
|
26
29
|
async setup(options, nuxt) {
|
|
27
30
|
registerModule(MODULE_NAME, nuxt);
|
|
31
|
+
addAuthScope(
|
|
32
|
+
nuxt,
|
|
33
|
+
{ name: "forms:manage", description: "Manage forms and submissions (staff)" },
|
|
34
|
+
{ name: "forms:submit", description: "Submit forms as you" },
|
|
35
|
+
{ name: "forms:pii", description: "See submission answers and file names through MCP tools (also needs forms:manage)" }
|
|
36
|
+
);
|
|
28
37
|
const resolver = createResolver(import.meta.url);
|
|
38
|
+
registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
|
|
29
39
|
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
30
40
|
const settings = options.settings ?? {};
|
|
31
|
-
const withMenus = hasNuxtModule("@frigear-nu/menu");
|
|
32
41
|
const formPath = resolvePagePath(settings.pages?.form, "/forms/:slug");
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
42
|
+
const withAdmin = hasNuxtModule("@frigear-nu/admin");
|
|
43
|
+
const adminPage = (setting, ...segments) => withAdmin ? resolvePagePath(setting, adminRoute(nuxt, "forms", ...segments)) : void 0;
|
|
44
|
+
const adminPath = adminPage(settings.pages?.admin);
|
|
45
|
+
const adminFormPath = adminPage(settings.pages?.adminForm, ":id");
|
|
46
|
+
const adminSubmissionsPath = adminPage(settings.pages?.adminSubmissions, ":id", "submissions");
|
|
47
|
+
const adminSubmissionPath = adminPage(settings.pages?.adminSubmission, ":id", "submissions", ":submissionId");
|
|
37
48
|
nuxt.options.runtimeConfig.forms = {
|
|
38
49
|
rateLimit: settings.rateLimit ?? 10,
|
|
39
50
|
rateLimitWindowSeconds: settings.rateLimitWindowSeconds ?? 600,
|
|
40
|
-
seed: { enabled: options.seed?.enabled ?? false, forms: options.seed?.forms ?? [] }
|
|
51
|
+
seed: { enabled: options.seed?.enabled ?? false, forms: options.seed?.forms ?? [] },
|
|
52
|
+
captcha: {
|
|
53
|
+
turnstile: { secretKey: options.captcha?.turnstile?.secretKey ?? "" },
|
|
54
|
+
recaptcha: { secretKey: options.captcha?.recaptcha?.secretKey ?? "" },
|
|
55
|
+
hcaptcha: { secretKey: options.captcha?.hcaptcha?.secretKey ?? "" }
|
|
56
|
+
},
|
|
57
|
+
encryptionKey: options.encryptionKey ?? ""
|
|
41
58
|
};
|
|
42
59
|
nuxt.options.runtimeConfig.public.forms = {
|
|
43
60
|
pages: {
|
|
@@ -46,6 +63,12 @@ const module$1 = defineNuxtModule({
|
|
|
46
63
|
adminForm: adminFormPath,
|
|
47
64
|
adminSubmissions: adminSubmissionsPath,
|
|
48
65
|
adminSubmission: adminSubmissionPath
|
|
66
|
+
},
|
|
67
|
+
captcha: {
|
|
68
|
+
turnstile: { siteKey: options.captcha?.turnstile?.siteKey ?? "" },
|
|
69
|
+
recaptcha: { siteKey: options.captcha?.recaptcha?.siteKey ?? "" },
|
|
70
|
+
hcaptcha: { siteKey: options.captcha?.hcaptcha?.siteKey ?? "" },
|
|
71
|
+
defaultProvider: options.captcha?.defaultProvider ?? "turnstile"
|
|
49
72
|
}
|
|
50
73
|
};
|
|
51
74
|
await registerDatabaseTable(nuxt, {
|
|
@@ -58,44 +81,51 @@ const module$1 = defineNuxtModule({
|
|
|
58
81
|
route("get", "/admin/forms/:id", "admin/form.get");
|
|
59
82
|
route("patch", "/admin/forms/:id", "admin/form.patch");
|
|
60
83
|
route("delete", "/admin/forms/:id", "admin/form.delete");
|
|
84
|
+
route("post", "/admin/forms/:id/clone", "admin/form-clone.post");
|
|
61
85
|
route("get", "/admin/forms/:id/submissions", "admin/submissions.get");
|
|
86
|
+
route("get", "/admin/forms/:id/tokens", "admin/form-tokens.get");
|
|
87
|
+
route("post", "/admin/forms/:id/tokens", "admin/form-tokens.post");
|
|
88
|
+
route("delete", "/admin/forms/:id/tokens/:tokenId", "admin/form-token.delete");
|
|
89
|
+
route("get", "/admin/forms/:id/password", "admin/form-password.get");
|
|
90
|
+
route("put", "/admin/forms/:id/password", "admin/form-password.put");
|
|
91
|
+
route("delete", "/admin/forms/:id/password", "admin/form-password.delete");
|
|
92
|
+
route("get", "/admin/forms/:id/secrets", "admin/form-secrets.get");
|
|
93
|
+
route("put", "/admin/forms/:id/secrets", "admin/form-secrets.put");
|
|
94
|
+
route("delete", "/admin/forms/:id/secrets/:key", "admin/form-secret.delete");
|
|
62
95
|
route("post", "/admin/import", "admin/import.post");
|
|
63
96
|
route("get", "/admin/submissions/:id", "admin/submission.get");
|
|
64
97
|
route("get", "/admin/submissions/:id/files/:fileId", "admin/submission-file.get");
|
|
65
98
|
route("post", "/admin/submissions/:id/redeliver", "admin/submission-redeliver.post");
|
|
66
99
|
route("get", "/:slug", "public/form.get");
|
|
100
|
+
route("get", "/:slug/access", "public/access.get");
|
|
67
101
|
route("post", "/:slug/submit", "public/submit.post");
|
|
68
102
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
103
|
+
inlineModuleRuntime(nuxt, resolver);
|
|
104
|
+
if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
105
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/encryption"));
|
|
106
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/staff-scopes"));
|
|
69
107
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/email-delivery"));
|
|
70
108
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/seed"));
|
|
71
109
|
addImports([
|
|
72
110
|
{ name: "useForm", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
111
|
+
{ name: "useFormAccess", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
112
|
+
{ name: "FORM_PASSWORD_HEADER", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
73
113
|
{ name: "useFormsLocale", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
114
|
+
{ name: "useFormsLocales", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
74
115
|
{ name: "useFormsPaths", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
116
|
+
{ name: "useFormLink", from: resolver.resolve("./runtime/app/composables/useForms") },
|
|
75
117
|
{ name: "useFormStepper", from: resolver.resolve("./runtime/app/composables/useFormStepper") }
|
|
76
118
|
]);
|
|
77
119
|
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{ name: "forms-
|
|
81
|
-
{ name: "forms-admin
|
|
82
|
-
{ name: "forms-admin-
|
|
83
|
-
{ name: "forms-admin-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const taken = new Set(existing.map((page) => page.path));
|
|
88
|
-
for (const page of pages) {
|
|
89
|
-
if (page.path && !taken.has(page.path)) {
|
|
90
|
-
existing.push(defineModulePage(MODULE_NAME, { name: page.name, path: page.path, file: resolver.resolve(`./runtime/app/pages/${page.file}`) }));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (withMenus) {
|
|
96
|
-
const { registerMenuSource } = await import('@frigear-nu/menu/utils');
|
|
97
|
-
registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
98
|
-
}
|
|
120
|
+
addComponent({ name: "FormEmbed", filePath: resolver.resolve("./runtime/app/components/FormEmbed.vue"), global: true });
|
|
121
|
+
addModulePages(MODULE_NAME, resolver, [
|
|
122
|
+
{ name: "forms-form", path: formPath, file: "./runtime/app/pages/form.vue" },
|
|
123
|
+
{ name: "forms-admin", path: adminPath, file: "./runtime/app/pages/admin.vue" },
|
|
124
|
+
{ name: "forms-admin-form", path: adminFormPath, file: "./runtime/app/pages/admin-form.vue" },
|
|
125
|
+
{ name: "forms-admin-submissions", path: adminSubmissionsPath, file: "./runtime/app/pages/admin-submissions.vue" },
|
|
126
|
+
{ name: "forms-admin-submission", path: adminSubmissionPath, file: "./runtime/app/pages/admin-submission.vue" }
|
|
127
|
+
]);
|
|
128
|
+
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
99
129
|
}
|
|
100
130
|
});
|
|
101
131
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormDefinition } from '../../../types.js';
|
|
2
|
+
/** The saved form; enables per-form secrets (captcha secret key), which are stored right away, not with the definition. */
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
formId?: string;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
modelValue: FormDefinition;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare var __VLS_82: {};
|
|
11
|
+
type __VLS_Slots = {} & {
|
|
12
|
+
preview?: (props: typeof __VLS_82) => any;
|
|
13
|
+
};
|
|
14
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:modelValue": (value: FormDefinition) => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
17
|
+
"onUpdate:modelValue"?: ((value: FormDefinition) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|