@frigear-nu/forms 0.0.6 → 0.0.7
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 +64 -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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireFormsManager(event);
|
|
5
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
await db.delete(schema.formAccess).where(eq(schema.formAccess.formId, form.id));
|
|
7
|
+
return { set: false };
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireFormsManager(event);
|
|
5
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
const [row] = await db.select({ formId: schema.formAccess.formId }).from(schema.formAccess).where(eq(schema.formAccess.formId, form.id)).limit(1);
|
|
7
|
+
return { set: !!row };
|
|
8
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
const input = z.object({ password: z.string().min(6).max(200) });
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await requireFormsManager(event);
|
|
6
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
7
|
+
const { password } = await readValidatedBody(event, input.parse);
|
|
8
|
+
const passwordHash = await hashPassword(password);
|
|
9
|
+
await db.insert(schema.formAccess).values({ formId: form.id, passwordHash }).onConflictDoUpdate({
|
|
10
|
+
target: schema.formAccess.formId,
|
|
11
|
+
set: { passwordHash, updatedAt: /* @__PURE__ */ new Date() }
|
|
12
|
+
});
|
|
13
|
+
return { set: true };
|
|
14
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
await requireFormsManager(event);
|
|
3
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
4
|
+
const key = getRouterParam(event, "key");
|
|
5
|
+
if (!isFormSecretKey(key)) throw createError({ statusCode: 400, statusMessage: "Unknown secret key." });
|
|
6
|
+
await deleteFormSecret(form.id, key);
|
|
7
|
+
return describeFormSecrets(event, form.id);
|
|
8
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const input = z.object({
|
|
3
|
+
key: z.string().refine(isFormSecretKey, "Unknown secret key."),
|
|
4
|
+
value: z.string().trim().min(1).max(2e3)
|
|
5
|
+
});
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
await requireFormsManager(event);
|
|
8
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
9
|
+
const { key, value } = await readValidatedBody(event, input.parse);
|
|
10
|
+
await setFormSecret(form.id, key, value);
|
|
11
|
+
return describeFormSecrets(event, form.id);
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireFormsManager(event);
|
|
5
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
const [row] = await db.update(schema.formAccessTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(
|
|
7
|
+
eq(schema.formAccessTokens.id, getRouterParam(event, "tokenId")),
|
|
8
|
+
eq(schema.formAccessTokens.formId, form.id)
|
|
9
|
+
)).returning();
|
|
10
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Token not found." });
|
|
11
|
+
return toTokenDto(row);
|
|
12
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { desc, eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireFormsManager(event);
|
|
5
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
const rows = await db.select().from(schema.formAccessTokens).where(eq(schema.formAccessTokens.formId, form.id)).orderBy(desc(schema.formAccessTokens.createdAt));
|
|
7
|
+
return rows.map(toTokenDto);
|
|
8
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
const input = z.object({ name: z.string().trim().min(1).max(100) });
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await requireFormsManager(event);
|
|
6
|
+
const form = await findFormById(getRouterParam(event, "id"));
|
|
7
|
+
const { name } = await readValidatedBody(event, input.parse);
|
|
8
|
+
const { id, token, tokenHash, prefix } = await generateToken();
|
|
9
|
+
const [row] = await db.insert(schema.formAccessTokens).values({ id, formId: form.id, name, tokenHash, prefix }).returning();
|
|
10
|
+
setResponseStatus(event, 201);
|
|
11
|
+
return { ...toTokenDto(row), token };
|
|
12
|
+
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { count, eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
await requireFormsManager(event);
|
|
3
|
-
|
|
5
|
+
const row = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
const [totals] = await db.select({ total: count() }).from(schema.formSubmissions).where(eq(schema.formSubmissions.formId, row.id));
|
|
7
|
+
return { ...toAdminForm(row), submissionCount: totals?.total ?? 0 };
|
|
4
8
|
});
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
|
-
import { db, schema } from "@nuxthub/db";
|
|
3
1
|
import { formPatch } from "../../utils/admin-schemas.js";
|
|
4
2
|
export default defineEventHandler(async (event) => {
|
|
5
3
|
await requireFormsManager(event);
|
|
6
|
-
const
|
|
4
|
+
const id = getRouterParam(event, "id");
|
|
5
|
+
await findFormById(id);
|
|
7
6
|
const input = await readValidatedBody(event, formPatch.parse);
|
|
8
|
-
|
|
9
|
-
throw createError({ statusCode: 409, statusMessage: "A form with this slug already exists." });
|
|
10
|
-
}
|
|
11
|
-
const definitionChanged = input.definition !== void 0 && JSON.stringify(input.definition) !== JSON.stringify(current.definition);
|
|
12
|
-
const [row] = await db.update(schema.forms).set({
|
|
13
|
-
...input,
|
|
14
|
-
version: definitionChanged ? current.version + 1 : current.version
|
|
15
|
-
}).where(eq(schema.forms.id, current.id)).returning();
|
|
16
|
-
return toAdminForm(row);
|
|
7
|
+
return toAdminForm(await patchForm(id, input));
|
|
17
8
|
});
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { db, schema } from "@nuxthub/db";
|
|
2
|
-
import { newId } from "../../../utils/index.js";
|
|
3
1
|
import { formInput } from "../../utils/admin-schemas.js";
|
|
4
2
|
export default defineEventHandler(async (event) => {
|
|
5
3
|
await requireFormsManager(event);
|
|
6
4
|
const input = await readValidatedBody(event, formInput.parse);
|
|
7
|
-
|
|
8
|
-
throw createError({ statusCode: 409, statusMessage: "A form with this slug already exists." });
|
|
9
|
-
}
|
|
10
|
-
const [row] = await db.insert(schema.forms).values({ id: newId("frm"), ...input }).returning();
|
|
5
|
+
const row = await createForm(input);
|
|
11
6
|
setResponseStatus(event, 201);
|
|
12
7
|
return toAdminForm(row);
|
|
13
8
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
|
|
2
|
+
const form = await findPublishedForm(getRouterParam(event, "slug"));
|
|
3
|
+
await authorizeFormAccess(event, form);
|
|
4
|
+
return toPublicForm(form);
|
|
3
5
|
});
|
|
@@ -2,6 +2,6 @@ export default defineEventHandler(async (event) => {
|
|
|
2
2
|
const slug = getRouterParam(event, "slug");
|
|
3
3
|
await enforceRateLimit(event, slug);
|
|
4
4
|
const form = await findPublishedForm(slug);
|
|
5
|
-
const
|
|
6
|
-
return processSubmission(event, form,
|
|
5
|
+
const { userId } = await authorizeFormAccess(event, form);
|
|
6
|
+
return processSubmission(event, form, userId);
|
|
7
7
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import { schema } from '@nuxthub/db';
|
|
3
|
+
import type { FormAccessInfoDto } from '../../../types.js';
|
|
4
|
+
type FormRow = typeof schema.forms.$inferSelect;
|
|
5
|
+
export declare const TOKEN_HEADER = "x-form-token";
|
|
6
|
+
export declare const PASSWORD_HEADER = "x-form-password";
|
|
7
|
+
export declare function hashToken(token: string): Promise<string>;
|
|
8
|
+
/** A new random token (shown once), its hash and its display prefix. */
|
|
9
|
+
export declare function generateToken(): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
token: string;
|
|
12
|
+
tokenHash: string;
|
|
13
|
+
prefix: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** `Authorization: Bearer <token>` or `x-form-token: <token>`. */
|
|
16
|
+
export declare function readToken(event: H3Event): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The single access gate of the public routes (definition and submit). Resolves the submitter's user id
|
|
19
|
+
* (when signed in) and throws 401 unless the form's `access.auth` is satisfied. API-only forms are
|
|
20
|
+
* still served here: `availability` only removes the public page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function authorizeFormAccess(event: H3Event, form: FormRow): Promise<{
|
|
23
|
+
userId: number | null;
|
|
24
|
+
}>;
|
|
25
|
+
/** Public facts the page needs to pick between the form, a gate and "not found". */
|
|
26
|
+
export declare function describeFormAccess(event: H3Event, form: FormRow): Promise<FormAccessInfoDto>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { isFormOpen, newId, nextFormOpening, resolveAccess } from "../../utils/index.js";
|
|
4
|
+
export const TOKEN_HEADER = "x-form-token";
|
|
5
|
+
export const PASSWORD_HEADER = "x-form-password";
|
|
6
|
+
const TOKEN_PREFIX = "ffk_";
|
|
7
|
+
const toHex = (bytes) => [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
export async function hashToken(token) {
|
|
9
|
+
return toHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)));
|
|
10
|
+
}
|
|
11
|
+
export async function generateToken() {
|
|
12
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
13
|
+
const token = `${TOKEN_PREFIX}${btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "")}`;
|
|
14
|
+
return { id: newId("fat"), token, tokenHash: await hashToken(token), prefix: token.slice(0, 8) };
|
|
15
|
+
}
|
|
16
|
+
export function readToken(event) {
|
|
17
|
+
const authorization = getRequestHeader(event, "authorization")?.trim() ?? "";
|
|
18
|
+
const bearer = authorization.slice(0, 7).toLowerCase() === "bearer " ? authorization.slice(7).trim() : "";
|
|
19
|
+
return bearer || getRequestHeader(event, TOKEN_HEADER)?.trim() || void 0;
|
|
20
|
+
}
|
|
21
|
+
const unauthorized = (event, message, scheme) => {
|
|
22
|
+
if (scheme) setResponseHeader(event, "www-authenticate", scheme);
|
|
23
|
+
return createError({ statusCode: 401, statusMessage: message, data: { access: true } });
|
|
24
|
+
};
|
|
25
|
+
async function verifyToken(event, form) {
|
|
26
|
+
const token = readToken(event);
|
|
27
|
+
if (!token) throw unauthorized(event, "A form token is required.", "Bearer");
|
|
28
|
+
const [row] = await db.select({ id: schema.formAccessTokens.id }).from(schema.formAccessTokens).where(and(
|
|
29
|
+
eq(schema.formAccessTokens.formId, form.id),
|
|
30
|
+
eq(schema.formAccessTokens.tokenHash, await hashToken(token)),
|
|
31
|
+
isNull(schema.formAccessTokens.revokedAt)
|
|
32
|
+
)).limit(1);
|
|
33
|
+
if (!row) throw unauthorized(event, "Invalid or revoked form token.", "Bearer");
|
|
34
|
+
await db.update(schema.formAccessTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq(schema.formAccessTokens.id, row.id));
|
|
35
|
+
}
|
|
36
|
+
async function verifyFormPassword(event, form) {
|
|
37
|
+
const password = getRequestHeader(event, PASSWORD_HEADER);
|
|
38
|
+
if (!password) throw unauthorized(event, "A form password is required.");
|
|
39
|
+
await assertPasswordAttemptsLeft(event, form.slug);
|
|
40
|
+
const [row] = await db.select().from(schema.formAccess).where(eq(schema.formAccess.formId, form.id)).limit(1);
|
|
41
|
+
if (!row || !await verifyPassword(row.passwordHash, password)) {
|
|
42
|
+
await recordPasswordFailure(event, form.slug);
|
|
43
|
+
throw unauthorized(event, "Wrong form password.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function authorizeFormAccess(event, form) {
|
|
47
|
+
const { auth, schedule } = resolveAccess(form.definition);
|
|
48
|
+
if (!isFormOpen(schedule)) {
|
|
49
|
+
const opensAt = nextFormOpening(schedule);
|
|
50
|
+
throw createError({ statusCode: 403, statusMessage: "This form is closed.", data: { closed: true, opensAt: opensAt?.toISOString() ?? null } });
|
|
51
|
+
}
|
|
52
|
+
const user = await getUser(event, { scopes: ["forms:submit"] });
|
|
53
|
+
if (auth === "user" && !user) throw unauthorized(event, "Sign in to use this form.");
|
|
54
|
+
if (auth === "token") await verifyToken(event, form);
|
|
55
|
+
if (auth === "password") await verifyFormPassword(event, form);
|
|
56
|
+
return { userId: user?.id ?? null };
|
|
57
|
+
}
|
|
58
|
+
export async function describeFormAccess(event, form) {
|
|
59
|
+
const user = await getUser(event, { scopes: ["forms:submit"] });
|
|
60
|
+
const access = resolveAccess(form.definition);
|
|
61
|
+
const open = isFormOpen(access.schedule);
|
|
62
|
+
return { slug: form.slug, name: form.name, access, signedIn: !!user, open, opensAt: open ? null : nextFormOpening(access.schedule)?.toISOString() ?? null };
|
|
63
|
+
}
|