@frigear-nu/forms 0.1.2 → 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/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 +9 -6
- 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 +15 -10
- 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,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
3
|
import { db, schema } from "@nuxthub/db";
|
|
4
|
+
import { isoDate } from "@frigear-nu/module-kit/server";
|
|
4
5
|
export default defineScopedMcpTool({
|
|
5
6
|
description: "Get one submission (staff). Answers, submitter and file names are only included with the forms:pii scope.",
|
|
6
7
|
scopes: ["forms:manage"],
|
|
@@ -8,18 +9,17 @@ export default defineScopedMcpTool({
|
|
|
8
9
|
annotations: { readOnlyHint: true },
|
|
9
10
|
inputSchema: { id: z.string() },
|
|
10
11
|
handler: async ({ id }, ctx) => {
|
|
11
|
-
const
|
|
12
|
-
if (!row) throw createError({ statusCode: 404, statusMessage: "Submission not found." });
|
|
12
|
+
const row = await findSubmissionById(id);
|
|
13
13
|
const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
|
|
14
14
|
const allow = ctx.hasScope("forms:pii");
|
|
15
15
|
const submission = redactPii({
|
|
16
16
|
...pick(row, ["id", "formId", "formSlug", "formVersion", "locale", "deliveryStatus"]),
|
|
17
|
-
createdAt: row.createdAt
|
|
17
|
+
createdAt: isoDate(row.createdAt),
|
|
18
18
|
answeredFields: Object.keys(row.answers),
|
|
19
19
|
fileCount: files.length,
|
|
20
20
|
answers: row.answers,
|
|
21
21
|
userId: row.userId,
|
|
22
|
-
files: files.map(
|
|
22
|
+
files: files.map(toSubmissionFileDto)
|
|
23
23
|
}, { allow, fields: ["answers", "userId", "files"] });
|
|
24
24
|
return { notice: piiNotice(allow, "forms:pii", "Answers, the submitter and file names"), submission };
|
|
25
25
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { submissionLocale } from "../../utils/submit.js";
|
|
2
3
|
export default defineScopedMcpTool({
|
|
3
4
|
description: "Submit a published form as yourself. Use get-form first to see the fields.",
|
|
4
5
|
scopes: ["forms:submit"],
|
|
@@ -6,7 +7,7 @@ export default defineScopedMcpTool({
|
|
|
6
7
|
inputSchema: {
|
|
7
8
|
slug: z.string(),
|
|
8
9
|
answers: z.record(z.string(), z.unknown()).describe("Answers keyed by field key."),
|
|
9
|
-
locale:
|
|
10
|
+
locale: submissionLocale.optional()
|
|
10
11
|
},
|
|
11
12
|
handler: async ({ slug, answers, locale }, { event, user }) => {
|
|
12
13
|
await enforceRateLimit(event, slug);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { created, idParams, uniqueCopySlug } from "@frigear-nu/module-kit/server";
|
|
2
3
|
import { newId } from "../../../utils/index.js";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const source = await findFormById(id);
|
|
8
|
+
const slug = await uniqueCopySlug(source.slug, findFormBySlug);
|
|
8
9
|
const [row] = await db.insert(schema.forms).values({
|
|
9
10
|
id: newId("frm"),
|
|
10
11
|
slug,
|
|
@@ -12,6 +13,5 @@ export default defineEventHandler(async (event) => {
|
|
|
12
13
|
status: "draft",
|
|
13
14
|
definition: source.definition
|
|
14
15
|
}).returning();
|
|
15
|
-
|
|
16
|
-
return toAdminForm(row);
|
|
16
|
+
return created(event, toAdminForm(row));
|
|
17
17
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams, noContent } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
5
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const form = await findFormById(id);
|
|
6
8
|
await db.delete(schema.formAccess).where(eq(schema.formAccess.formId, form.id));
|
|
7
|
-
return
|
|
9
|
+
return noContent(event);
|
|
8
10
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const form = await findFormById(id);
|
|
6
8
|
const [row] = await db.select({ formId: schema.formAccess.formId }).from(schema.formAccess).where(eq(schema.formAccess.formId, form.id)).limit(1);
|
|
7
9
|
return { set: !!row };
|
|
8
10
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
const input = z.object({ password: z.string().min(6).max(200) });
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
|
-
await
|
|
6
|
-
const
|
|
6
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
7
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
8
|
const { password } = await readValidatedBody(event, input.parse);
|
|
9
|
+
const form = await findFormById(id);
|
|
8
10
|
const passwordHash = await hashPassword(password);
|
|
9
11
|
await db.insert(schema.formAccess).values({ formId: form.id, passwordHash }).onConflictDoUpdate({
|
|
10
12
|
target: schema.formAccess.formId,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { noContent } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { secretParams } from "../../utils/admin-schemas.js";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
await
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
if (!isFormSecretKey(key)) throw createError({ statusCode: 400, statusMessage: "Unknown secret key." });
|
|
4
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
5
|
+
const { id, key } = await getValidatedRouterParams(event, secretParams.parse);
|
|
6
|
+
const form = await findFormById(id);
|
|
6
7
|
await deleteFormSecret(form.id, key);
|
|
7
|
-
return
|
|
8
|
+
return noContent(event);
|
|
8
9
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { secretParams } from "../../utils/admin-schemas.js";
|
|
3
|
+
const input = z.object({ value: z.string().trim().min(1).max(2e3) });
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
6
|
+
const { id, key } = await getValidatedRouterParams(event, secretParams.parse);
|
|
7
|
+
const { value } = await readValidatedBody(event, input.parse);
|
|
8
|
+
const form = await findFormById(id);
|
|
9
|
+
await setFormSecret(form.id, key, value);
|
|
10
|
+
return describeFormSecrets(event, form.id);
|
|
11
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
await
|
|
3
|
-
const
|
|
3
|
+
await formsStaff.require(event);
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
const form = await findFormById(id);
|
|
4
6
|
return describeFormSecrets(event, form.id);
|
|
5
7
|
});
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams, noContent, notFound } from "@frigear-nu/module-kit/server";
|
|
4
|
+
const params = idParams().extend(idParams("tokenId").shape);
|
|
3
5
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
7
|
+
const { id, tokenId } = await getValidatedRouterParams(event, params.parse);
|
|
8
|
+
const form = await findFormById(id);
|
|
6
9
|
const [row] = await db.update(schema.formAccessTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(
|
|
7
|
-
eq(schema.formAccessTokens.id,
|
|
10
|
+
eq(schema.formAccessTokens.id, tokenId),
|
|
8
11
|
eq(schema.formAccessTokens.formId, form.id)
|
|
9
|
-
)).returning();
|
|
10
|
-
if (!row) throw
|
|
11
|
-
return
|
|
12
|
+
)).returning({ id: schema.formAccessTokens.id });
|
|
13
|
+
if (!row) throw notFound("Token not found.");
|
|
14
|
+
return noContent(event);
|
|
12
15
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { desc, eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const form = await findFormById(id);
|
|
6
8
|
const rows = await db.select().from(schema.formAccessTokens).where(eq(schema.formAccessTokens.formId, form.id)).orderBy(desc(schema.formAccessTokens.createdAt));
|
|
7
|
-
return rows.map(
|
|
9
|
+
return rows.map(toFormTokenDto);
|
|
8
10
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { created, idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
const input = z.object({ name: z.string().trim().min(1).max(100) });
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
|
-
await
|
|
6
|
-
const
|
|
6
|
+
await formsStaff.require(event, { sessionOnly: true });
|
|
7
|
+
const { id: formId } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
8
|
const { name } = await readValidatedBody(event, input.parse);
|
|
9
|
+
const form = await findFormById(formId);
|
|
8
10
|
const { id, token, tokenHash, prefix } = await generateToken();
|
|
9
11
|
const [row] = await db.insert(schema.formAccessTokens).values({ id, formId: form.id, name, tokenHash, prefix }).returning();
|
|
10
|
-
|
|
11
|
-
return { ...toTokenDto(row), token };
|
|
12
|
+
return created(event, { ...toFormTokenDto(row), token });
|
|
12
13
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams, noContent } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const current = await findFormById(id);
|
|
8
|
+
await db.update(schema.forms).set({ status: "archived" }).where(eq(schema.forms.id, current.id));
|
|
9
|
+
return noContent(event);
|
|
8
10
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { count, eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const row = await findFormById(id);
|
|
6
8
|
const [totals] = await db.select({ total: count() }).from(schema.formSubmissions).where(eq(schema.formSubmissions.formId, row.id));
|
|
7
9
|
return { ...toAdminForm(row), submissionCount: totals?.total ?? 0 };
|
|
8
10
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
1
2
|
import { formPatch } from "../../utils/admin-schemas.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
|
-
await
|
|
4
|
-
const id =
|
|
5
|
-
await findFormById(id);
|
|
4
|
+
await formsStaff.require(event);
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
6
6
|
const input = await readValidatedBody(event, formPatch.parse);
|
|
7
7
|
return toAdminForm(await patchForm(id, input));
|
|
8
8
|
});
|
|
@@ -1,19 +1,40 @@
|
|
|
1
|
-
import { count,
|
|
1
|
+
import { and, count, eq, inArray, ne, or, sql } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { likeContains, orderByQuery, pageQuery, pageRange, paginate } from "@frigear-nu/module-kit/server";
|
|
5
|
+
const querySchema = pageQuery({
|
|
6
|
+
sort: ["updatedAt", "createdAt", "name", "slug", "status", "submissionCount"],
|
|
7
|
+
defaultSort: "updatedAt",
|
|
8
|
+
defaultOrder: "desc",
|
|
9
|
+
filters: {
|
|
10
|
+
status: z.enum(["draft", "published", "archived"]).optional(),
|
|
11
|
+
/** `false` hides archived forms (ignored when `status` is set). */
|
|
12
|
+
archived: z.enum(["true", "false"]).optional()
|
|
13
|
+
}
|
|
14
|
+
});
|
|
3
15
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
await formsStaff.require(event);
|
|
17
|
+
const query = await getValidatedQuery(event, querySchema.parse);
|
|
18
|
+
const conditions = [];
|
|
19
|
+
if (query.q) conditions.push(or(likeContains(schema.forms.name, query.q), likeContains(schema.forms.slug, query.q)));
|
|
20
|
+
if (query.status) conditions.push(eq(schema.forms.status, query.status));
|
|
21
|
+
else if (query.archived === "false") conditions.push(ne(schema.forms.status, "archived"));
|
|
22
|
+
const where = conditions.length ? and(...conditions) : void 0;
|
|
23
|
+
const submissionCount = sql`(select count(*) from ${schema.formSubmissions} where ${schema.formSubmissions.formId} = ${schema.forms.id})`;
|
|
24
|
+
const orderBy = orderByQuery({
|
|
25
|
+
updatedAt: schema.forms.updatedAt,
|
|
26
|
+
createdAt: schema.forms.createdAt,
|
|
27
|
+
name: schema.forms.name,
|
|
28
|
+
slug: schema.forms.slug,
|
|
29
|
+
status: schema.forms.status,
|
|
30
|
+
submissionCount
|
|
31
|
+
}, query, { tiebreaker: schema.forms.id });
|
|
32
|
+
const { limit, offset } = pageRange(query);
|
|
33
|
+
const [rows, [totals]] = await Promise.all([
|
|
34
|
+
db.select().from(schema.forms).where(where).orderBy(...orderBy).limit(limit).offset(offset),
|
|
35
|
+
db.select({ total: count() }).from(schema.forms).where(where)
|
|
36
|
+
]);
|
|
37
|
+
const counts = rows.length ? await db.select({ formId: schema.formSubmissions.formId, total: count() }).from(schema.formSubmissions).where(inArray(schema.formSubmissions.formId, rows.map((row) => row.id))).groupBy(schema.formSubmissions.formId) : [];
|
|
38
|
+
const totalsByForm = new Map(counts.map((row) => [row.formId, row.total]));
|
|
39
|
+
return paginate({ items: rows.map((row) => toFormSummaryDto(row, totalsByForm.get(row.id) ?? 0)), total: totals?.total ?? 0, query });
|
|
19
40
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { created } from "@frigear-nu/module-kit/server";
|
|
1
2
|
import { formInput } from "../../utils/admin-schemas.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
|
-
await
|
|
4
|
+
await formsStaff.require(event);
|
|
4
5
|
const input = await readValidatedBody(event, formInput.parse);
|
|
5
|
-
|
|
6
|
-
setResponseStatus(event, 201);
|
|
7
|
-
return toAdminForm(row);
|
|
6
|
+
return created(event, toAdminForm(await createForm(input)));
|
|
8
7
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { importInput } from "../../utils/admin-schemas.js";
|
|
2
2
|
export default defineEventHandler(async (event) => {
|
|
3
|
-
await
|
|
3
|
+
await formsStaff.require(event);
|
|
4
4
|
const { forms } = await readValidatedBody(event, importInput.parse);
|
|
5
|
-
|
|
5
|
+
const { inserted, skipped } = await seedForms(forms.map((form) => ({ ...form, definition: form.definition })));
|
|
6
|
+
return { inserted, skipped };
|
|
6
7
|
});
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
3
|
import { blob } from "@nuxthub/blob";
|
|
4
|
+
import { idParams, notFound } from "@frigear-nu/module-kit/server";
|
|
5
|
+
const params = idParams().extend(idParams("fileId").shape);
|
|
4
6
|
export default defineEventHandler(async (event) => {
|
|
5
|
-
await
|
|
7
|
+
await formsStaff.require(event);
|
|
8
|
+
const { id, fileId } = await getValidatedRouterParams(event, params.parse);
|
|
6
9
|
const [file] = await db.select().from(schema.formSubmissionFiles).where(and(
|
|
7
|
-
eq(schema.formSubmissionFiles.submissionId,
|
|
8
|
-
eq(schema.formSubmissionFiles.id,
|
|
10
|
+
eq(schema.formSubmissionFiles.submissionId, id),
|
|
11
|
+
eq(schema.formSubmissionFiles.id, fileId)
|
|
9
12
|
)).limit(1);
|
|
10
|
-
if (!file) throw
|
|
13
|
+
if (!file) throw notFound("File not found.");
|
|
11
14
|
const stored = await blob.get(file.pathname);
|
|
12
|
-
if (!stored) throw
|
|
15
|
+
if (!stored) throw notFound("File not found.");
|
|
13
16
|
setResponseHeader(event, "content-type", file.contentType);
|
|
14
17
|
setResponseHeader(event, "content-disposition", `attachment; filename="${encodeURIComponent(file.filename)}"`);
|
|
15
18
|
setResponseHeader(event, "x-content-type-options", "nosniff");
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const row = await findSubmissionById(id);
|
|
7
8
|
const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
|
|
8
9
|
const [form] = await db.select().from(schema.forms).where(eq(schema.forms.id, row.formId)).limit(1);
|
|
9
10
|
const ctx = {
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
3
4
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
await
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
await formsStaff.require(event);
|
|
6
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
7
|
+
const row = await findSubmissionById(id);
|
|
7
8
|
const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
|
|
8
|
-
return
|
|
9
|
-
id: row.id,
|
|
10
|
-
formId: row.formId,
|
|
11
|
-
formSlug: row.formSlug,
|
|
12
|
-
formVersion: row.formVersion,
|
|
13
|
-
definition: row.definitionSnapshot,
|
|
14
|
-
answers: row.answers,
|
|
15
|
-
files: files.map(({ id, fieldKey, filename, contentType, size }) => ({ id, fieldKey, filename, contentType, size })),
|
|
16
|
-
locale: row.locale,
|
|
17
|
-
userId: row.userId,
|
|
18
|
-
deliveryStatus: row.deliveryStatus,
|
|
19
|
-
deliveryError: row.deliveryError,
|
|
20
|
-
createdAt: row.createdAt.toISOString()
|
|
21
|
-
};
|
|
9
|
+
return toSubmissionDto(row, files);
|
|
22
10
|
});
|
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { and, count, eq } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
|
-
import {
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { idParams, orderByQuery, pageQuery, pageRange, paginate } from "@frigear-nu/module-kit/server";
|
|
5
|
+
const querySchema = pageQuery({
|
|
6
|
+
sort: ["createdAt", "deliveryStatus", "locale"],
|
|
7
|
+
defaultSort: "createdAt",
|
|
8
|
+
defaultOrder: "desc",
|
|
9
|
+
filters: {
|
|
10
|
+
deliveryStatus: z.enum(["none", "pending", "delivered", "failed"]).optional()
|
|
11
|
+
}
|
|
12
|
+
});
|
|
4
13
|
export default defineEventHandler(async (event) => {
|
|
5
|
-
await
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
await formsStaff.require(event);
|
|
15
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
16
|
+
const query = await getValidatedQuery(event, querySchema.parse);
|
|
17
|
+
const form = await findFormById(id);
|
|
18
|
+
const conditions = [eq(schema.formSubmissions.formId, form.id)];
|
|
19
|
+
if (query.deliveryStatus) conditions.push(eq(schema.formSubmissions.deliveryStatus, query.deliveryStatus));
|
|
20
|
+
const where = and(...conditions);
|
|
21
|
+
const orderBy = orderByQuery({
|
|
22
|
+
createdAt: schema.formSubmissions.createdAt,
|
|
23
|
+
deliveryStatus: schema.formSubmissions.deliveryStatus,
|
|
24
|
+
locale: schema.formSubmissions.locale
|
|
25
|
+
}, query, { tiebreaker: schema.formSubmissions.id });
|
|
26
|
+
const { limit, offset } = pageRange(query);
|
|
27
|
+
const [rows, [totals]] = await Promise.all([
|
|
28
|
+
db.select().from(schema.formSubmissions).where(where).orderBy(...orderBy).limit(limit).offset(offset),
|
|
29
|
+
db.select({ total: count() }).from(schema.formSubmissions).where(where)
|
|
30
|
+
]);
|
|
31
|
+
return paginate({ items: rows.map(toSubmissionSummaryDto), total: totals?.total ?? 0, query });
|
|
16
32
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { slugParams } from "../../utils/admin-schemas.js";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
|
|
3
|
+
const { slug } = await getValidatedRouterParams(event, slugParams.parse);
|
|
4
|
+
return describeFormAccess(event, await findPublishedForm(slug));
|
|
3
5
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { slugParams } from "../../utils/admin-schemas.js";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
const
|
|
3
|
+
const { slug } = await getValidatedRouterParams(event, slugParams.parse);
|
|
4
|
+
const form = await findPublishedForm(slug);
|
|
3
5
|
await authorizeFormAccess(event, form);
|
|
4
6
|
return toPublicForm(form);
|
|
5
7
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { slugParams } from "../../utils/admin-schemas.js";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
|
-
const slug =
|
|
3
|
+
const { slug } = await getValidatedRouterParams(event, slugParams.parse);
|
|
3
4
|
await enforceRateLimit(event, slug);
|
|
4
5
|
const form = await findPublishedForm(slug);
|
|
5
6
|
const { userId } = await authorizeFormAccess(event, form);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { apiError, isoDate } from "@frigear-nu/module-kit/server";
|
|
1
2
|
import { and, eq, isNull } from "drizzle-orm";
|
|
2
3
|
import { db, schema } from "@nuxthub/db";
|
|
3
4
|
import { isFormOpen, newId, nextFormOpening, resolveAccess } from "../../utils/index.js";
|
|
@@ -20,7 +21,7 @@ export function readToken(event) {
|
|
|
20
21
|
}
|
|
21
22
|
const unauthorized = (event, message, scheme) => {
|
|
22
23
|
if (scheme) setResponseHeader(event, "www-authenticate", scheme);
|
|
23
|
-
return
|
|
24
|
+
return apiError(401, "unauthorized", message, { access: true });
|
|
24
25
|
};
|
|
25
26
|
async function verifyToken(event, form) {
|
|
26
27
|
const token = readToken(event);
|
|
@@ -47,7 +48,7 @@ export async function authorizeFormAccess(event, form) {
|
|
|
47
48
|
const { auth, schedule } = resolveAccess(form.definition);
|
|
48
49
|
if (!isFormOpen(schedule)) {
|
|
49
50
|
const opensAt = nextFormOpening(schedule);
|
|
50
|
-
throw
|
|
51
|
+
throw apiError(403, "form_closed", "This form is closed.", { closed: true, opensAt: isoDate(opensAt) });
|
|
51
52
|
}
|
|
52
53
|
const user = await getUser(event, { scopes: ["forms:submit"] });
|
|
53
54
|
if (auth === "user" && !user) throw unauthorized(event, "Sign in to use this form.");
|
|
@@ -493,3 +493,12 @@ export declare const importInput: z.ZodObject<{
|
|
|
493
493
|
}>>;
|
|
494
494
|
}, z.core.$strip>>;
|
|
495
495
|
}, z.core.$strip>;
|
|
496
|
+
/** Router params of the public routes (`/api/forms/:slug`). */
|
|
497
|
+
export declare const slugParams: z.ZodObject<{
|
|
498
|
+
slug: z.ZodString;
|
|
499
|
+
}, z.core.$strip>;
|
|
500
|
+
/** Router params of `/api/admin/forms/:id/secrets/:key`; an unknown key is a 400. */
|
|
501
|
+
export declare const secretParams: z.ZodObject<{
|
|
502
|
+
id: z.ZodString;
|
|
503
|
+
key: z.ZodString;
|
|
504
|
+
}, z.core.$strip>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
3
|
import { formDefinitionSchema } from "../../utils/index.js";
|
|
4
|
+
import { isFormSecretKey } from "./secrets.js";
|
|
3
5
|
const slug = z.string().min(1).max(100).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, digits and dashes.").refine((value) => value !== "admin", "This slug is reserved.");
|
|
4
6
|
const statusSchema = z.enum(["draft", "published", "archived"]);
|
|
5
7
|
export const formInput = z.object({
|
|
@@ -12,3 +14,5 @@ export const formPatch = formInput.partial().extend({ status: statusSchema.optio
|
|
|
12
14
|
export const importInput = z.object({
|
|
13
15
|
forms: z.array(formInput.extend({ status: statusSchema.optional() })).min(1).max(50)
|
|
14
16
|
});
|
|
17
|
+
export const slugParams = idParams("slug");
|
|
18
|
+
export const secretParams = idParams().extend({ key: z.string().refine(isFormSecretKey, "Unknown secret key.") });
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The staff gate of the admin routes (form definitions, submissions, files, access credentials): the `forms:manage`
|
|
4
|
+
* scope for bearer credentials, then the host's `forms:authorize` Nitro hook (denied by default).
|
|
5
|
+
*
|
|
6
|
+
* await formsStaff.require(event) // reads and ordinary edits
|
|
7
|
+
* await formsStaff.require(event, { sessionOnly: true }) // minting or removing access credentials
|
|
8
|
+
*/
|
|
9
|
+
export declare const formsStaff: any;
|
|
6
10
|
/** Non-throwing form of the `forms:authorize` policy for the caller of an already-authenticated request (MCP). */
|
|
7
11
|
export declare const canManageForms: (event: H3Event, principal: {
|
|
8
12
|
user: {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const canManageForms = (event, principal) => gate.isAllowed(event, principal.user.id);
|
|
1
|
+
export const formsStaff = defineStaffGuard({
|
|
2
|
+
hook: "forms:authorize",
|
|
3
|
+
scope: "forms:manage",
|
|
4
|
+
message: "You are not allowed to manage forms."
|
|
5
|
+
});
|
|
6
|
+
export const canManageForms = (event, principal) => formsStaff.can(event, principal.user.id);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { apiError } from "@frigear-nu/module-kit/server";
|
|
1
2
|
const VERIFY_URLS = {
|
|
2
3
|
turnstile: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
3
4
|
recaptcha: "https://www.google.com/recaptcha/api/siteverify",
|
|
@@ -11,7 +12,7 @@ export async function verifyCaptcha(event, provider, token, formId) {
|
|
|
11
12
|
if (ctx.valid !== void 0) return ctx.valid;
|
|
12
13
|
if (!secret) {
|
|
13
14
|
console.error(`[forms] captcha provider "${provider}" has no secret key: set one on the form or NUXT_FORMS_CAPTCHA_${provider.toUpperCase()}_SECRET_KEY.`);
|
|
14
|
-
throw
|
|
15
|
+
throw apiError(500, "not_configured", `The ${provider} captcha is not configured on the server.`, { captcha: true });
|
|
15
16
|
}
|
|
16
17
|
const body = new URLSearchParams({ secret, response: token });
|
|
17
18
|
if (ip) body.set("remoteip", ip);
|
|
@@ -20,6 +21,6 @@ export async function verifyCaptcha(event, provider, token, formId) {
|
|
|
20
21
|
return result?.success === true;
|
|
21
22
|
} catch (cause) {
|
|
22
23
|
console.error(`[forms] ${provider} verification request failed:`, cause);
|
|
23
|
-
throw
|
|
24
|
+
throw apiError(502, "captcha_unavailable", "The captcha could not be verified. Please try again.", { captcha: true });
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { schema } from '@nuxthub/db';
|
|
2
|
-
import type { FormAccessTokenDto, FormAdminDto, PublicFormDto } from '../../../types.js';
|
|
2
|
+
import type { FormAccessTokenDto, FormAdminDto, FormSummaryDto, PublicFormDto, SubmissionDto, SubmissionFileDto, SubmissionSummaryDto } from '../../../types.js';
|
|
3
3
|
import type { z } from 'zod';
|
|
4
4
|
import type { formInput, formPatch } from './admin-schemas.js';
|
|
5
5
|
type FormRow = typeof schema.forms.$inferSelect;
|
|
6
6
|
export declare const toPublicForm: (row: FormRow) => PublicFormDto;
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const toFormTokenDto: (row: typeof schema.formAccessTokens.$inferSelect) => FormAccessTokenDto;
|
|
8
8
|
export declare const toAdminForm: (row: FormRow) => FormAdminDto;
|
|
9
|
+
/** A row of the admin form list. */
|
|
10
|
+
export declare const toFormSummaryDto: (row: FormRow, submissionCount: number) => FormSummaryDto;
|
|
11
|
+
type SubmissionRow = typeof schema.formSubmissions.$inferSelect;
|
|
12
|
+
type SubmissionFileRow = typeof schema.formSubmissionFiles.$inferSelect;
|
|
13
|
+
/** A row of a form's submission list: the first few answers as a preview. */
|
|
14
|
+
export declare const toSubmissionSummaryDto: (row: SubmissionRow) => SubmissionSummaryDto;
|
|
15
|
+
/** File metadata of a submission; the blob pathname stays on the server. */
|
|
16
|
+
export declare const toSubmissionFileDto: (row: SubmissionFileRow) => SubmissionFileDto;
|
|
17
|
+
export declare const toSubmissionDto: (row: SubmissionRow, files: SubmissionFileRow[]) => SubmissionDto;
|
|
18
|
+
export declare function findSubmissionById(id: string): Promise<SubmissionRow>;
|
|
9
19
|
export declare function findFormById(id: string): Promise<FormRow>;
|
|
10
20
|
export declare function findFormBySlug(slug: string): Promise<FormRow | undefined>;
|
|
11
21
|
export declare function findPublishedForm(slug: string): Promise<FormRow>;
|