@frigear-nu/forms 0.1.2 → 0.1.4
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/FormField.vue +1 -1
- 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 +37 -21
- 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,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { formsApi } from "../../utils";
|
|
2
3
|
const route = useRoute();
|
|
3
4
|
const t = useModuleT("forms");
|
|
4
5
|
const format = useFormat();
|
|
@@ -21,7 +22,7 @@ async function unlock() {
|
|
|
21
22
|
checking.value = true;
|
|
22
23
|
wrong.value = false;
|
|
23
24
|
try {
|
|
24
|
-
await $fetch(
|
|
25
|
+
await $fetch(formsApi.form(slug.value), { headers: { [FORM_PASSWORD_HEADER]: password.value } });
|
|
25
26
|
unlocked.value = password.value;
|
|
26
27
|
} catch {
|
|
27
28
|
wrong.value = true;
|
|
@@ -112,7 +112,6 @@
|
|
|
112
112
|
"clearFilters": "Ryd filtre",
|
|
113
113
|
"noMatches": "Ingen formularer matcher.",
|
|
114
114
|
"emptyDescription": "Opret en formular for at begynde at indsamle svar.",
|
|
115
|
-
"showing": "{shown} af {total} formularer",
|
|
116
115
|
"open": "Åbn",
|
|
117
116
|
"updated": "Opdateret",
|
|
118
117
|
"cloned": "Formular duplikeret",
|
|
@@ -126,7 +125,14 @@
|
|
|
126
125
|
"details": "Detaljer",
|
|
127
126
|
"submissionNotFound": "Besvarelsen findes ikke.",
|
|
128
127
|
"noSubmissionsHint": "Svar vises her, når nogen udfylder formularen.",
|
|
129
|
-
"redelivered": "Leveret igen"
|
|
128
|
+
"redelivered": "Leveret igen",
|
|
129
|
+
"importEmpty": "Indsæt den JSON, der skal importeres.",
|
|
130
|
+
"importLabel": "Formularer som JSON",
|
|
131
|
+
"loadFailed": "Formularerne kunne ikke indlæses",
|
|
132
|
+
"tryAgain": "Prøv igen",
|
|
133
|
+
"formLoadFailed": "Formularen kunne ikke indlæses",
|
|
134
|
+
"submissionsLoadFailed": "Besvarelserne kunne ikke indlæses",
|
|
135
|
+
"submissionLoadFailed": "Besvarelsen kunne ikke indlæses"
|
|
130
136
|
},
|
|
131
137
|
"schedule": {
|
|
132
138
|
"closed": "Formularen er lukket.",
|
|
@@ -138,6 +144,7 @@
|
|
|
138
144
|
"hint": "Uden for perioden vises en lukket-besked, og indsendelser afvises. En sæson, der slutter før den starter, løber over nytår.",
|
|
139
145
|
"opens": "Åbner",
|
|
140
146
|
"closes": "Lukker",
|
|
147
|
+
"month": "Måned",
|
|
141
148
|
"day": "Dag",
|
|
142
149
|
"time": "Tid",
|
|
143
150
|
"timezone": "Tidszone"
|
|
@@ -198,6 +205,7 @@
|
|
|
198
205
|
"failed": "Levering fejlede",
|
|
199
206
|
"title": "Levering og lagring",
|
|
200
207
|
"storeTitle": "Gem besvarelser",
|
|
208
|
+
"storageTitle": "Lagring",
|
|
201
209
|
"storeHint": "Gem besvarelser i databasen, så de kan ses under Besvarelser.",
|
|
202
210
|
"filesWarning": "Formularen har filupload-felter, som kræver gemte besvarelser. Fjern dem, eller fortsæt med at gemme.",
|
|
203
211
|
"lostWarning": "Besvarelser hverken gemmes eller sendes på mail. Tilføj modtagere eller fortsæt med at gemme, ellers går svarene tabt, medmindre noget håndterer forms:submitted.",
|
|
@@ -112,7 +112,6 @@
|
|
|
112
112
|
"clearFilters": "Clear filters",
|
|
113
113
|
"noMatches": "No forms match.",
|
|
114
114
|
"emptyDescription": "Create a form to start collecting answers.",
|
|
115
|
-
"showing": "{shown} of {total} forms",
|
|
116
115
|
"open": "Open",
|
|
117
116
|
"updated": "Updated",
|
|
118
117
|
"cloned": "Form duplicated",
|
|
@@ -126,7 +125,14 @@
|
|
|
126
125
|
"details": "Details",
|
|
127
126
|
"submissionNotFound": "This submission does not exist.",
|
|
128
127
|
"noSubmissionsHint": "Answers appear here once someone submits the form.",
|
|
129
|
-
"redelivered": "Delivered again"
|
|
128
|
+
"redelivered": "Delivered again",
|
|
129
|
+
"importEmpty": "Paste the JSON to import.",
|
|
130
|
+
"importLabel": "Forms as JSON",
|
|
131
|
+
"loadFailed": "Forms could not be loaded",
|
|
132
|
+
"tryAgain": "Try again",
|
|
133
|
+
"formLoadFailed": "The form could not be loaded",
|
|
134
|
+
"submissionsLoadFailed": "Submissions could not be loaded",
|
|
135
|
+
"submissionLoadFailed": "The submission could not be loaded"
|
|
130
136
|
},
|
|
131
137
|
"schedule": {
|
|
132
138
|
"closed": "This form is closed.",
|
|
@@ -138,6 +144,7 @@
|
|
|
138
144
|
"hint": "Outside the period the form shows a closed notice and rejects submissions. A season whose end is before its start wraps over new year.",
|
|
139
145
|
"opens": "Opens",
|
|
140
146
|
"closes": "Closes",
|
|
147
|
+
"month": "Month",
|
|
141
148
|
"day": "Day",
|
|
142
149
|
"time": "Time",
|
|
143
150
|
"timezone": "Timezone"
|
|
@@ -198,6 +205,7 @@
|
|
|
198
205
|
"failed": "Delivery failed",
|
|
199
206
|
"title": "Delivery & storage",
|
|
200
207
|
"storeTitle": "Store submissions",
|
|
208
|
+
"storageTitle": "Storage",
|
|
201
209
|
"storeHint": "Keep submissions in the database so they can be reviewed under Submissions.",
|
|
202
210
|
"filesWarning": "This form has file upload fields, which need stored submissions. Remove them or keep storing.",
|
|
203
211
|
"lostWarning": "Submissions are neither stored nor emailed. Add recipients or keep storing, or answers are lost unless something handles forms:submitted.",
|
|
@@ -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>;
|