diff --git a/.gitignore b/.gitignore index f7dc85b5..6d4bf6d4 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ typings/ reports **/tsconfig.tsbuildinfo tsconfig.tsbuildinfo +uploads/ diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 22d6446d..6a7694e6 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -23,6 +23,18 @@ "host": "MEILISEARCH_HOST", "apiKey": "MEILISEARCH_KEY" }, + "fileDefaultProvider": "FILE_DEFAULT_PROVIDER", + "fileProviders": { + "LOCAL": { + "path":"FILE_PROVIDER_LOCAL_PATH" + }, + "AZURE": { + "account": "FILE_PROVIDER_AZURE_ACCOUNT", + "accountKey": "FILE_PROVIDER_AZURE_ACCOUNT_KEY", + "container": "FILE_PROVIDER_AZURE_CONTAINER", + "folder": "FILE_PROVIDER_AZURE_FOLDER" + } + }, "tomtom": { "apiKey": "TOMTOM_APIKEY" }, diff --git a/api/config/default.json b/api/config/default.json index be2052af..2546a68e 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -31,6 +31,18 @@ "host": "meilisearch:7700", "apiKey": "xbAPupQKhkKvF176vE2JxAdPGpmWVu251Hldn6K4Z6Y" }, + "fileDefaultProvider": "LOCAL", + "fileProviders": { + "LOCAL": { + "path":"../uploads" + }, + "AZURE": { + "account": "", + "accountKey": "", + "container": "", + "folder": "" + } + }, "tomtom": { "apiKey": "" }, diff --git a/api/package.json b/api/package.json index 279730ab..58e9f042 100644 --- a/api/package.json +++ b/api/package.json @@ -24,6 +24,7 @@ "seed": "tsx prisma/seeders/index.ts" }, "dependencies": { + "@azure/storage-blob": "^12.17.0", "@faker-js/faker": "^8.4.1", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", @@ -45,6 +46,7 @@ "koa-session": "^6.4.0", "koa-static": "^5.0.0", "meilisearch": "^0.37.0", + "mime-types": "^2.1.35", "mjml": "^4.15.3", "prom-client": "^15.0.0", "superjson": "^2.2.1", diff --git a/api/prisma/migrations/20241118163314_init_file/migration.sql b/api/prisma/migrations/20241118163314_init_file/migration.sql new file mode 100644 index 00000000..33e407ff --- /dev/null +++ b/api/prisma/migrations/20241118163314_init_file/migration.sql @@ -0,0 +1,42 @@ +-- CreateEnum +CREATE TYPE "FileProvider" AS ENUM ('LOCAL', 'AZURE'); + +-- CreateTable +CREATE TABLE "File" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "uploaded" BOOLEAN NOT NULL DEFAULT false, + "uploadedAt" TIMESTAMP(3), + "provider" "FileProvider" NOT NULL, + "key" TEXT NOT NULL, + "filename" TEXT, + "mimetype" TEXT, + + CONSTRAINT "File_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UnterveranstaltungDocument" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "unterveranstaltungId" INTEGER NOT NULL, + "fileId" TEXT NOT NULL, + + CONSTRAINT "UnterveranstaltungDocument_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "File_id_key" ON "File"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "File_key_key" ON "File"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "UnterveranstaltungDocument_fileId_key" ON "UnterveranstaltungDocument"("fileId"); + +-- AddForeignKey +ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/api/prisma/schema/File.prisma b/api/prisma/schema/File.prisma new file mode 100644 index 00000000..7355e8d7 --- /dev/null +++ b/api/prisma/schema/File.prisma @@ -0,0 +1,16 @@ +enum FileProvider { + LOCAL + AZURE +} + +model File { + id String @id @unique @default(uuid()) + createdAt DateTime @default(now()) + uploaded Boolean @default(false) + uploadedAt DateTime? + provider FileProvider + key String @unique() + filename String? + mimetype String? + UnterveranstaltungDocument UnterveranstaltungDocument? +} diff --git a/api/prisma/schema/Unterveranstaltung.prisma b/api/prisma/schema/Unterveranstaltung.prisma index a828e671..e857e578 100644 --- a/api/prisma/schema/Unterveranstaltung.prisma +++ b/api/prisma/schema/Unterveranstaltung.prisma @@ -18,4 +18,15 @@ model Unterveranstaltung { bedingungen String? type UnterveranstaltungType @default(GLIEDERUNG) customFields CustomField[] + documents UnterveranstaltungDocument[] +} + +model UnterveranstaltungDocument { + id Int @id @default(autoincrement()) + name String + description String? + unterveranstaltungId Int + unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade) + file File @relation(fields: [fileId], references: [id]) + fileId String @unique } diff --git a/api/src/azureStorage.ts b/api/src/azureStorage.ts new file mode 100644 index 00000000..719f5016 --- /dev/null +++ b/api/src/azureStorage.ts @@ -0,0 +1,30 @@ +import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob' + +import config from './config' + +const isAzureConfigured = + config.fileProviders.AZURE.account !== '' && + config.fileProviders.AZURE.accountKey !== '' && + config.fileProviders.AZURE.container !== '' + +async function init() { + if (!isAzureConfigured) return null + const account = config.fileProviders.AZURE.account + const accountKey = config.fileProviders.AZURE.accountKey + const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey) + + const blobServiceClient = new BlobServiceClient(`https://${account}.blob.core.windows.net`, sharedKeyCredential) + + // Check if container exists, if not create it + const containerClient = blobServiceClient.getContainerClient(config.fileProviders.AZURE.container) + if (!(await containerClient.exists())) await containerClient.create() + return blobServiceClient +} + +export let azureStorage: BlobServiceClient | null = null + +init() + .then((blobServiceClient) => (azureStorage = blobServiceClient)) + .catch((e) => { + console.error('Failed to initialize Azure Blob Storage', e) + }) diff --git a/api/src/config.ts b/api/src/config.ts index 0d7ed7da..452f6f72 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -1,6 +1,7 @@ import path from 'node:path' import { fileURLToPath } from 'url' +import { FileProvider } from '@prisma/client' import config from 'config' import { z } from 'zod' @@ -43,6 +44,19 @@ export const configSchema = z.strictObject({ host: z.string(), apiKey: z.string(), }), + fileDefaultProvider: z.nativeEnum(FileProvider), + fileProviders: z.strictObject({ + LOCAL: z.strictObject({ + path: z.string(), + }), + AZURE: z.strictObject({ + account: z.string(), + accountKey: z.string(), + container: z.string(), + folder: z.string(), + }), + }), + tomtom: z.strictObject({ apiKey: z.string(), }), diff --git a/api/src/middleware/downloadFileLocal.ts b/api/src/middleware/downloadFileLocal.ts new file mode 100644 index 00000000..e4f43a61 --- /dev/null +++ b/api/src/middleware/downloadFileLocal.ts @@ -0,0 +1,34 @@ +import * as fs from 'fs' +import * as path from 'path' + +import type { Middleware } from 'koa' +import mime from 'mime-types' + +import config from '../config' +import prisma from '../prisma' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const downloadFileLocal: Middleware = async function (ctx, next) { + const fileId = ctx.params.id + const file = await prisma.file.findFirst({ + where: { + id: fileId, + }, + }) + if (file === null) { + ctx.response.status = 404 + return + } + + if (file.provider !== 'LOCAL') { + ctx.response.status = 404 + return + } + + const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path) + const mimetype = file.mimetype ?? 'application/octet-stream' + const filename = file.filename ?? `${file.id}.${mime.extension(mimetype)}` + ctx.set('Content-disposition', `attachment; filename=${filename}`) + ctx.set('Content-type', mimetype) + ctx.response.body = fs.createReadStream(uploadDir + '/' + file.key) +} diff --git a/api/src/middleware/index.ts b/api/src/middleware/index.ts index 7f98723b..bb10bbb7 100644 --- a/api/src/middleware/index.ts +++ b/api/src/middleware/index.ts @@ -1,9 +1,13 @@ import type Router from 'koa-router' +import { downloadFileLocal } from './downloadFileLocal' import { importAnmeldungen } from './importAnmeldungen' +import { uploadFileLocal } from './uploadFileLocal' export default function addMiddlewares(router: Router) { router.post('/upload/anmeldungen', async (ctx, next) => { return await importAnmeldungen(ctx, next) }) + router.post('/upload/file/LOCAL/:id', uploadFileLocal) + router.get('/download/file/LOCAL/:id', downloadFileLocal) } diff --git a/api/src/middleware/uploadFileLocal.ts b/api/src/middleware/uploadFileLocal.ts new file mode 100644 index 00000000..41c23cdb --- /dev/null +++ b/api/src/middleware/uploadFileLocal.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs/promises' +import * as path from 'path' + +import type { Middleware } from 'koa' + +import config from '../config' +import prisma from '../prisma' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const uploadFileLocal: Middleware = async function (ctx, next) { + const fileId = ctx.params.id + const file = await prisma.file.findFirst({ + where: { + id: fileId, + }, + }) + if (file === null) { + ctx.response.status = 400 + ctx.response.body = { error: `File with id '${fileId}' not found` } + return + } + + if (file.uploaded) { + ctx.response.status = 400 + ctx.response.body = { error: `File with id '${fileId}' already uploaded` } + return + } + + if (file.provider !== 'LOCAL') { + ctx.response.status = 400 + ctx.response.body = { error: `File provider is '${file.provider}'. This endpoint is for LOCAL` } + return + } + + const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path) + try { + await checkLocalUploadFolder(uploadDir) + } catch (e) { + console.error('Error while creating upload-directory\n', e) + ctx.response.status = 500 + ctx.response.body = { + error: 'Something went wrong during creation of upload-directory', + } + return + } + + const fileData = ctx.request.files?.file + if (!fileData || Array.isArray(fileData)) { + ctx.response.status = 400 + ctx.response.body = { + error: 'No or Invalid File provided', + } + return + } + + try { + await fs.copyFile(fileData.filepath, uploadDir + '/' + file.key) + } catch (e) { + console.error('Error while copy to upload-directory\n', e) + ctx.response.status = 500 + ctx.response.body = { + error: 'Something went wrong during copy to upload-directory', + } + return + } + + await prisma.file.update({ + where: { id: fileId }, + data: { + mimetype: fileData.mimetype ?? 'application/octet-stream', + filename: fileData.originalFilename ?? undefined, + uploaded: true, + uploadedAt: new Date(), + }, + }) + + ctx.response.status = 201 + ctx.response.body = { uploaded: true } +} + +async function checkLocalUploadFolder(uploadDir: string) { + try { + await fs.stat(uploadDir) + } catch (e: any) { + if (e.code === 'ENOENT') await fs.mkdir(uploadDir, { recursive: true }) + else throw e + } +} diff --git a/api/src/services/file/file.router.ts b/api/src/services/file/file.router.ts new file mode 100644 index 00000000..efde75d2 --- /dev/null +++ b/api/src/services/file/file.router.ts @@ -0,0 +1,12 @@ +/* eslint-disable prettier/prettier */ // Prettier ignored is because this file is generated +import { mergeRouters } from '../../trpc' + +import { fileCreateProcedure } from './fileCreate' +import { fileGetUrlActionProcedure } from './fileGetUrl' +// Import Routes here - do not delete this line + +export const fileRouter = mergeRouters( + fileCreateProcedure.router, + fileGetUrlActionProcedure.router, + // Add Routes here - do not delete this line +) diff --git a/api/src/services/file/fileCreate.ts b/api/src/services/file/fileCreate.ts new file mode 100644 index 00000000..d3080948 --- /dev/null +++ b/api/src/services/file/fileCreate.ts @@ -0,0 +1,60 @@ +import { randomUUID } from 'crypto' + +import { BlobSASPermissions } from '@azure/storage-blob' +import dayjs from 'dayjs' +import z from 'zod' + +import { azureStorage } from '../../azureStorage' +import config from '../../config' +import prisma from '../../prisma' +import { defineProcedure } from '../../types/defineProcedure' + +export const fileCreateProcedure = defineProcedure({ + key: 'fileCreate', + method: 'mutation', + protection: { + type: 'restrictToRoleIds', + 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, + } + }, +}) diff --git a/api/src/services/file/fileGetUrl.ts b/api/src/services/file/fileGetUrl.ts new file mode 100644 index 00000000..0e7c5dc4 --- /dev/null +++ b/api/src/services/file/fileGetUrl.ts @@ -0,0 +1,47 @@ +import { BlobSASPermissions } from '@azure/storage-blob' +import dayjs from 'dayjs' +import z from 'zod' + +import { azureStorage } from '../../azureStorage' +import config from '../../config' +import prisma from '../../prisma' +import { defineProcedure } from '../../types/defineProcedure' + +const downloadUrlLifespan = 60 * 60 // 1 hour + +export const fileGetUrlActionProcedure = defineProcedure({ + key: 'fileGetUrl', + method: 'query', + protection: { type: 'public' }, + inputSchema: z.strictObject({ + id: z.string().uuid(), + }), + async handler(options) { + const file = await prisma.file.findUniqueOrThrow({ + where: { + id: options.input.id, + }, + select: { + id: true, + provider: true, + uploaded: true, + key: true, + }, + }) + + if (file.provider === 'LOCAL') { + if (!file.uploaded) throw new Error('File is not uploaded') + return new URL(`/api/download/file/${file.provider}/${file.id}`, config.clientUrl).href + } + if (file.provider === 'AZURE' && azureStorage !== null) { + const containerClient = azureStorage.getContainerClient(config.fileProviders.AZURE.container) + const blockBlobClient = containerClient.getBlockBlobClient(file.key) + return await blockBlobClient.generateSasUrl({ + startsOn: dayjs().subtract(5, 'minute').toDate(), + expiresOn: dayjs().add(downloadUrlLifespan, 'seconds').toDate(), + permissions: BlobSASPermissions.from({ read: true }), + }) + } + return null + }, +}) diff --git a/api/src/services/index.ts b/api/src/services/index.ts index 1e776478..ca7aa9a8 100644 --- a/api/src/services/index.ts +++ b/api/src/services/index.ts @@ -6,6 +6,7 @@ import { addressRouter } from './address/address.router' import { anmeldungRouter } from './anmeldung/anmeldung.router' import { authenticationRouter } from './authentication/authentication.router' import { customFieldsRouter } from './customFields/customFields.router' +import { fileRouter } from './file/file.router' import { gliederungRouter } from './gliederung/gliederung.router' import { ortRouter } from './ort/ort.router' import { personRouter } from './person/person.router' @@ -28,6 +29,7 @@ export const serviceRouter = router({ search: searchRouter, system: systemRouter, customFields: customFieldsRouter, + file: fileRouter, address: addressRouter, // Add Routers here - do not delete this line }) diff --git a/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts b/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts index 631e5507..37410e05 100644 --- a/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts +++ b/api/src/services/unterveranstaltung/unterveranstaltungGliederungGet.ts @@ -44,6 +44,14 @@ export const unterveranstaltungGliederungGetProcedure = defineProcedure({ }, }, gliederung: true, + documents: { + select: { + id: true, + name: true, + description: true, + fileId: true, + }, + }, }, }) }, diff --git a/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts b/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts index 5c80315a..1396b17a 100644 --- a/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts +++ b/api/src/services/unterveranstaltung/unterveranstaltungGliederungPatch.ts @@ -1,3 +1,5 @@ +import type { UUID } from 'crypto' + import { Role } from '@prisma/client' import z from 'zod' @@ -18,16 +20,56 @@ export const unterveranstaltungGliederungPatchProcedure = defineProcedure({ meldeschluss: z.date().optional(), beschreibung: z.string().optional(), bedingungen: z.string().optional(), + addDocuments: z + .array( + z.strictObject({ + name: z.string(), + fileId: z.string().uuid(), + }) + ) + .optional(), + updateDocuments: z.array(z.strictObject({ id: z.number().int(), name: z.string() })).optional(), + deleteDocumentIds: z.array(z.number().int()).optional(), }), }), async handler(options) { const gliederung = await getGliederungRequireAdmin(options.ctx.accountId) + + // Documents create, update, delete + const documents: { + createMany?: { data: { name: string; fileId: UUID }[] } + updateMany?: { where: { id: number }; data: { name: string } }[] + deleteMany?: { id: number }[] + } = {} + if (options.input.data.addDocuments) { + documents.createMany = { + data: options.input.data.addDocuments.map((doc) => ({ ...doc, fileId: doc.fileId as UUID })), + } + } + if (options.input.data.updateDocuments) { + documents.updateMany = options.input.data.updateDocuments.map((doc) => ({ + where: { id: doc.id }, + data: { name: doc.name }, + })) + } + if (options.input.data.deleteDocumentIds) { + documents.deleteMany = options.input.data.deleteDocumentIds.map((id) => ({ id })) + } + return prisma.unterveranstaltung.update({ where: { id: options.input.id, gliederungId: gliederung.id, }, - data: options.input.data, + data: { + maxTeilnehmende: options.input.data.maxTeilnehmende, + teilnahmegebuehr: options.input.data.teilnahmegebuehr, + meldebeginn: options.input.data.meldebeginn, + meldeschluss: options.input.data.meldeschluss, + beschreibung: options.input.data.beschreibung, + bedingungen: options.input.data.bedingungen, + documents: documents, + }, select: { id: true, }, diff --git a/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts b/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts index 6e5a2b88..d12c795e 100644 --- a/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts +++ b/api/src/services/unterveranstaltung/unterveranstaltungPublicGet.ts @@ -48,6 +48,12 @@ export const unterveranstaltungPublicGetProcedure = defineProcedure({ }, beschreibung: true, bedingungen: true, + documents: { + select: { + name: true, + fileId: true, + }, + }, }, }) return unterveranstaltung diff --git a/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts b/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts index 7958a10e..c07cd958 100644 --- a/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts +++ b/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungGet.ts @@ -49,6 +49,14 @@ export const unterveranstaltungVerwaltungGetProcedure = defineProcedure({ hostname: true, }, }, + documents: { + select: { + id: true, + name: true, + description: true, + fileId: true, + }, + }, }, }) }, diff --git a/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts b/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts index 1ca8b569..8f716708 100644 --- a/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts +++ b/api/src/services/unterveranstaltung/unterveranstaltungVerwaltungPatch.ts @@ -1,3 +1,5 @@ +import type { UUID } from 'crypto' + import { Role } from '@prisma/client' import z from 'zod' @@ -17,14 +19,53 @@ export const unterveranstaltungVerwaltungPatchProcedure = defineProcedure({ meldeschluss: z.date().optional(), beschreibung: z.string().optional(), bedingungen: z.string().optional(), + addDocuments: z + .array( + z.strictObject({ + name: z.string(), + fileId: z.string().uuid(), + }) + ) + .optional(), + updateDocuments: z.array(z.strictObject({ id: z.number().int(), name: z.string() })).optional(), + deleteDocumentIds: z.array(z.number().int()).optional(), }), }), async handler(options) { + // Documents create, update, delete + const documents: { + createMany?: { data: { name: string; fileId: UUID }[] } + updateMany?: { where: { id: number }; data: { name: string } }[] + deleteMany?: { id: number }[] + } = {} + if (options.input.data.addDocuments) { + documents.createMany = { + data: options.input.data.addDocuments.map((doc) => ({ ...doc, fileId: doc.fileId as UUID })), + } + } + if (options.input.data.updateDocuments) { + documents.updateMany = options.input.data.updateDocuments.map((doc) => ({ + where: { id: doc.id }, + data: { name: doc.name }, + })) + } + if (options.input.data.deleteDocumentIds) { + documents.deleteMany = options.input.data.deleteDocumentIds.map((id) => ({ id })) + } + return prisma.unterveranstaltung.update({ where: { id: options.input.id, }, - data: options.input.data, + data: { + maxTeilnehmende: options.input.data.maxTeilnehmende, + teilnahmegebuehr: options.input.data.teilnahmegebuehr, + meldebeginn: options.input.data.meldebeginn, + meldeschluss: options.input.data.meldeschluss, + beschreibung: options.input.data.beschreibung, + bedingungen: options.input.data.bedingungen, + documents: documents, + }, select: { id: true, }, diff --git a/chart/brahmsee-digital/templates/deployment-app.yaml b/chart/brahmsee-digital/templates/deployment-app.yaml index 62483373..d3d3fe36 100644 --- a/chart/brahmsee-digital/templates/deployment-app.yaml +++ b/chart/brahmsee-digital/templates/deployment-app.yaml @@ -65,6 +65,32 @@ spec: name: meilisearch key: key + + # FILE PROVIDER + - name: FILE_DEFAULT_PROVIDER + value: "{{ .Values.app.fileDefaultProvider }}" + - name: FILE_PROVIDER_LOCAL_PATH + value: "{{ .Values.app.fileProviders.LOCAL.path }}" + - name: FILE_PROVIDER_AZURE_ACCOUNT + valueFrom: + secretKeyRef: + name: azure + key: account + - name: FILE_PROVIDER_AZURE_ACCOUNT_KEY + valueFrom: + secretKeyRef: + name: azure + key: accountKey + - name: FILE_PROVIDER_AZURE_CONTAINER + valueFrom: + secretKeyRef: + name: azure + key: container + - name: FILE_PROVIDER_AZURE_FOLDER + valueFrom: + secretKeyRef: + name: azure + key: folder # Postgres - name: TOMTOM_APIKEY valueFrom: diff --git a/chart/brahmsee-digital/templates/secrets.yaml b/chart/brahmsee-digital/templates/secrets.yaml index 65f624e4..38a4e648 100644 --- a/chart/brahmsee-digital/templates/secrets.yaml +++ b/chart/brahmsee-digital/templates/secrets.yaml @@ -42,6 +42,19 @@ type: Opaque --- apiVersion: v1 kind: Secret +metadata: + name: azure + labels: + {{- include "codeanker.label" . | indent 4 }} +stringData: + account: {{ .Values.app.fileProviders.AZURE.account }} + accountKey: {{ .Values.app.fileProviders.AZURE.accountKey }} + container: {{ .Values.app.fileProviders.AZURE.container }} + folder: {{ .Values.app.fileProviders.AZURE.folder }} +type: Opaque +--- +apiVersion: v1 +kind: Secret metadata: name: tomtom labels: diff --git a/chart/brahmsee-digital/values.yaml b/chart/brahmsee-digital/values.yaml index a95bff63..1b610791 100644 --- a/chart/brahmsee-digital/values.yaml +++ b/chart/brahmsee-digital/values.yaml @@ -16,6 +16,15 @@ app: jwt_secret: "" meilisearch: key: 'defaultKey' + fileDefaultProvider: 'LOCAL' + fileProviders: + LOCAL: + path: /tmp + AZURE: + account: "", + accountKey: "", + container: "" + folder: "" postgres: enabled: true diff --git a/frontend/package.json b/frontend/package.json index 50481dd3..45848412 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "serve": "vite preview" }, "dependencies": { + "@azure/storage-blob": "^12.17.0", "@codeanker/api": "*", "@codeanker/cookies": "^1.0.0", "@codeanker/core-grid": "file:../vendor/codeanker-core-grid-1.0.0.tgz", diff --git a/frontend/src/components/DownloadLink.vue b/frontend/src/components/DownloadLink.vue new file mode 100644 index 00000000..cad5230d --- /dev/null +++ b/frontend/src/components/DownloadLink.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungGeneral.vue b/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungGeneral.vue index b1f495cc..08794590 100644 --- a/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungGeneral.vue +++ b/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungGeneral.vue @@ -1,4 +1,5 @@