diff --git a/apps/api/email/registration-successful.mjml b/apps/api/email/registration-successful.mjml index c78b0b3e..1e49badc 100644 --- a/apps/api/email/registration-successful.mjml +++ b/apps/api/email/registration-successful.mjml @@ -7,6 +7,19 @@ Vielen Dank für deine Anmeldung zur Veranstaltung {{ veranstaltung }}. +{{#if accessToken}} + + Bitte lade im nächsten Schritt ein Bild für {{ name }} hoch:  + Hier Foto hochladen + +{{/if}} + + +

Dazu befolgt du einfach die folgenden Schritte:

+ +
    +
  1. + Du erstellst dir einen Account auf + {{ hostname }} +
  2. +
  3. + Du gibst den unten genannten Zuordnungscode auf der folgenden Seite ein: + Anmeldung per Code zuordnen +
  4. +
+
+ Promise const seeders: Seeder[] = (() => { // in produktion nur Gliederungen importieren - if (isProduction()) { + if (isProduction() || process.env.DISABLE_SEEDER === '1') { return [importGliederungen] } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index f03ba503..99014228 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -26,7 +26,6 @@ app.use( 'img-src': ["'self'", '*.githubusercontent.com', 'data:', 'dlrgbrahmseedigitalprod.blob.core.windows.net'], 'connect-src': ["'self'", 'dlrgbrahmseedigitalprod.blob.core.windows.net'], }, - reportOnly: true, }, }) ) diff --git a/apps/api/src/services/anmeldung/anmeldung.router.ts b/apps/api/src/services/anmeldung/anmeldung.router.ts index 72465e36..f68c80d1 100644 --- a/apps/api/src/services/anmeldung/anmeldung.router.ts +++ b/apps/api/src/services/anmeldung/anmeldung.router.ts @@ -1,5 +1,7 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' +import { anmeldungAccessTokenValidateProcedure } from './anmeldungAccessTokenValidate.js' +import { anmeldungFotoUploadProcedure } from './anmeldungFotoUpload.js' import { anmeldungGetProcedure } from './anmeldungGet.js' import { anmeldungGliederungPatchProcedure } from './anmeldungGliederungPatch.js' @@ -26,6 +28,8 @@ export const anmeldungRouter = mergeRouters( anmeldungCountProcedure.router, anmeldungListProcedure.router, anmeldungGetProcedure.router, - anmeldungZuordnenProcedure.router + anmeldungZuordnenProcedure.router, + anmeldungAccessTokenValidateProcedure.router, + anmeldungFotoUploadProcedure.router // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts b/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts new file mode 100644 index 00000000..a906cd4b --- /dev/null +++ b/apps/api/src/services/anmeldung/anmeldungAccessTokenValidate.ts @@ -0,0 +1,44 @@ +import z from 'zod' + +import prisma from '../../prisma.js' +import { definePublicQueryProcedure } from '../../types/defineProcedure.js' + +export const anmeldungAccessTokenValidateProcedure = definePublicQueryProcedure({ + key: 'accessTokenValidate', + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + anmeldungId: z.number().int(), + accessToken: z.string().uuid(), + }), + handler: ({ input: { unterveranstaltungId, anmeldungId, accessToken } }) => + prisma.anmeldung.findFirst({ + where: { + unterveranstaltungId, + id: anmeldungId, + accessToken, + }, + select: { + person: { + select: { + id: true, + firstname: true, + lastname: true, + }, + }, + unterveranstaltung: { + select: { + veranstaltung: { + select: { + name: true, + }, + }, + gliederung: { + select: { + name: true, + }, + }, + }, + }, + }, + }), +}) diff --git a/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts new file mode 100644 index 00000000..240d3a61 --- /dev/null +++ b/apps/api/src/services/anmeldung/anmeldungFotoUpload.ts @@ -0,0 +1,32 @@ +import z from 'zod' + +import prisma from '../../prisma.js' + +import { definePublicMutateProcedure } from '../../types/defineProcedure.js' + +export const anmeldungFotoUploadProcedure = definePublicMutateProcedure({ + key: 'anmeldungFotoUpload', + inputSchema: z.strictObject({ + unterveranstaltungId: z.number().int(), + anmeldungId: z.number().int(), + accessToken: z.string().uuid(), + fileId: z.string(), + }), + handler: async ({ input: { unterveranstaltungId, anmeldungId, accessToken, fileId } }) => { + await prisma.anmeldung.update({ + where: { + unterveranstaltungId, + id: anmeldungId, + accessToken, + }, + data: { + accessToken: null, + person: { + update: { + photoId: fileId, + }, + }, + }, + }) + }, +}) diff --git a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts index 43d8a76a..8ec020ed 100644 --- a/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts +++ b/apps/api/src/services/anmeldung/anmeldungPublicCreate.ts @@ -71,6 +71,7 @@ export async function handle({ ctx, input, isPublic }: HandleProps) { }, }) + const accessToken = randomUUID() const assignmentCode = ctx.authenticated ? null : randomUUID() const anmeldung = await prisma.anmeldung.create({ data: { @@ -90,6 +91,7 @@ export async function handle({ ctx, input, isPublic }: HandleProps) { customFieldValues: { createMany: customFieldValuesCreateMany(input.customFieldValues), }, + accessToken, assignmentCode, }, }) @@ -119,7 +121,10 @@ export async function handle({ ctx, input, isPublic }: HandleProps) { gliederung: unterveranstaltung.gliederung.name, veranstaltung: unterveranstaltung.veranstaltung.name, hostname: unterveranstaltung.veranstaltung.hostname!.hostname, + unterveranstaltungId: unterveranstaltung.id, + anmeldungId: anmeldung.id, assignmentCode, + accessToken, }, }) } diff --git a/apps/api/src/services/file/anmeldungPublicFotoUpload.ts b/apps/api/src/services/file/anmeldungPublicFotoUpload.ts new file mode 100644 index 00000000..e2c385eb --- /dev/null +++ b/apps/api/src/services/file/anmeldungPublicFotoUpload.ts @@ -0,0 +1,31 @@ +import { z } from 'zod' +import { definePublicMutateProcedure } from '../../types/defineProcedure.js' +import { fileCreateSchema, handleFileUpload } from './fileCreate.js' +import client from '../../prisma.js' +import { TRPCError } from '@trpc/server' + +export const anmeldungPublicFotoUploadProcedure = definePublicMutateProcedure({ + key: 'anmeldungPublicFotoUpload', + inputSchema: fileCreateSchema.extend({ + unterveranstaltungId: z.number().int(), + anmeldungId: z.number().int(), + accessToken: z.string().uuid(), + }), + handler: async ({ input: { unterveranstaltungId, anmeldungId, accessToken, mimetype } }) => { + const anmeldung = await client.anmeldung.findFirst({ + where: { + unterveranstaltungId, + id: anmeldungId, + accessToken, + }, + }) + + if (anmeldung === null) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + }) + } + + return await handleFileUpload({ mimetype }) + }, +}) diff --git a/apps/api/src/services/file/file.router.ts b/apps/api/src/services/file/file.router.ts index dd53cb58..b91e3d63 100644 --- a/apps/api/src/services/file/file.router.ts +++ b/apps/api/src/services/file/file.router.ts @@ -1,5 +1,6 @@ // Prettier ignored is because this file is generated import { mergeRouters } from '../../trpc.js' +import { anmeldungPublicFotoUploadProcedure } from './anmeldungPublicFotoUpload.js' import { fileCreateProcedure } from './fileCreate.js' import { fileGetUrlActionProcedure } from './fileGetUrl.js' @@ -7,6 +8,7 @@ import { fileGetUrlActionProcedure } from './fileGetUrl.js' export const fileRouter = mergeRouters( fileCreateProcedure.router, - fileGetUrlActionProcedure.router + fileGetUrlActionProcedure.router, + anmeldungPublicFotoUploadProcedure.router // Add Routes here - do not delete this line ) diff --git a/apps/api/src/services/file/fileCreate.ts b/apps/api/src/services/file/fileCreate.ts index d55ea7ad..c13ab41b 100644 --- a/apps/api/src/services/file/fileCreate.ts +++ b/apps/api/src/services/file/fileCreate.ts @@ -9,48 +9,52 @@ import config from '../../config.js' import prisma from '../../prisma.js' import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js' +export const fileCreateSchema = z.strictObject({ + mimetype: z.string(), +}) + +export async function handleFileUpload(input: z.infer) { + const provider = config.fileDefaultProvider + let key: string = randomUUID() + if (provider === 'AZURE') { + key = `${config.fileProviders.AZURE.folder}/${key}` + } + + const file = await prisma.file.create({ + data: { + provider: provider, + key: key, + mimetype: input.mimetype, + }, + select: { + id: true, + provider: true, + uploaded: true, + }, + }) + + let azureUploadUrl: string | null = null + if (file.provider === 'AZURE' && azureStorage !== null) { + const containerClient = azureStorage.getContainerClient(config.fileProviders.AZURE.container) + const blockBlobClient = containerClient.getBlockBlobClient(key) + const permissions = BlobSASPermissions.from({ read: true, write: true, add: true, create: true }) + azureUploadUrl = await blockBlobClient.generateSasUrl({ + startsOn: dayjs().subtract(5, 'minute').toDate(), + expiresOn: dayjs().add(20, 'minute').toDate(), + permissions: permissions, + contentType: input.mimetype, + }) + } + + return { + ...file, + azureUploadUrl, + } +} + export const fileCreateProcedure = defineProtectedMutateProcedure({ key: 'fileCreate', roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'], - inputSchema: z.strictObject({ - mimetype: z.string(), - }), - async handler(options) { - const provider = config.fileDefaultProvider - let key: string = randomUUID() - if (provider === 'AZURE') { - key = `${config.fileProviders.AZURE.folder}/${key}` - } - - const file = await prisma.file.create({ - data: { - provider: provider, - key: key, - mimetype: options.input.mimetype, - }, - select: { - id: true, - provider: true, - uploaded: true, - }, - }) - - let azureUploadUrl: string | null = null - if (file.provider === 'AZURE' && azureStorage !== null) { - const containerClient = azureStorage.getContainerClient(config.fileProviders.AZURE.container) - const blockBlobClient = containerClient.getBlockBlobClient(key) - const permissions = BlobSASPermissions.from({ read: true, write: true, add: true, create: true }) - azureUploadUrl = await blockBlobClient.generateSasUrl({ - startsOn: dayjs().subtract(5, 'minute').toDate(), - expiresOn: dayjs().add(20, 'minute').toDate(), - permissions: permissions, - contentType: options.input.mimetype, - }) - } - - return { - ...file, - azureUploadUrl, - } - }, + inputSchema: fileCreateSchema, + handler: ({ input }) => handleFileUpload(input), }) diff --git a/apps/api/src/types/defineCustomFieldValues.ts b/apps/api/src/types/defineCustomFieldValues.ts index 618a5aa7..f681b768 100644 --- a/apps/api/src/types/defineCustomFieldValues.ts +++ b/apps/api/src/types/defineCustomFieldValues.ts @@ -5,7 +5,7 @@ export function defineCustomFieldValues() { return z.array( z.strictObject({ fieldId: z.number().int(), - value: z.union([z.string(), z.boolean(), z.undefined()]), + value: z.union([z.string(), z.boolean(), z.undefined(), z.number()]), }) ) } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 08631cdf..4ec38e0b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -38,6 +38,7 @@ "clsx": "^2.1.1", "http2-proxy": "^5.0.53", "intl-tel-input": "^24.4.0", + "primevue": "^4.2.5", "radix-vue": "^1.9.5", "remixicon": "^3.5.0", "simple-syntax-highlighter": "^3.1.1", diff --git a/apps/frontend/src/components/BasicInputs/BasicInputNumber.vue b/apps/frontend/src/components/BasicInputs/BasicInputNumber.vue index ec62901d..f7ee6e10 100644 --- a/apps/frontend/src/components/BasicInputs/BasicInputNumber.vue +++ b/apps/frontend/src/components/BasicInputs/BasicInputNumber.vue @@ -1,23 +1,28 @@ diff --git a/apps/frontend/src/components/CustomFields/CustomField.vue b/apps/frontend/src/components/CustomFields/CustomField.vue index e05189d3..d87d2b67 100644 --- a/apps/frontend/src/components/CustomFields/CustomField.vue +++ b/apps/frontend/src/components/CustomFields/CustomField.vue @@ -69,6 +69,7 @@ const model = computed({ v-model="model" :label="field.name" :required="field.required" + :min="0" /> >(() => [ // }, // { type: 'DividerItem', name: 'Ausschreibung', visible: hasPermissionToView(['ADMIN', 'GLIEDERUNG_ADMIN']) }, - { type: 'DividerItem', name: 'Meine Daten', visible: true }, + { type: 'DividerItem', name: 'Meine Daten', visible: hasPermissionToView(['USER']) }, { type: 'SidebarItem', name: 'Personen', route: { name: 'Meine Personen' }, icon: UsersIcon, - visible: true, + visible: hasPermissionToView(['USER']), }, { type: 'SidebarItem', name: 'Anmeldungen', route: { name: 'Meine Anmeldungen' }, icon: UsersIcon, - visible: true, + visible: hasPermissionToView(['USER']), }, { type: 'DividerItem', name: 'Gliederung', visible: hasPermissionToView(['GLIEDERUNG_ADMIN']) }, diff --git a/apps/frontend/src/components/forms/person/FormPersonGeneral.vue b/apps/frontend/src/components/forms/person/FormPersonGeneral.vue index 18285105..39b4204a 100644 --- a/apps/frontend/src/components/forms/person/FormPersonGeneral.vue +++ b/apps/frontend/src/components/forms/person/FormPersonGeneral.vue @@ -249,7 +249,7 @@ const submit = () => {