Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File Uploads #149

Open
wants to merge 15 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ typings/
reports
**/tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
uploads/
12 changes: 12 additions & 0 deletions api/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,17 @@
"meilisearch": {
"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"
}
}
}
12 changes: 12 additions & 0 deletions api/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,17 @@
"meilisearch": {
"host": "meilisearch:7700",
"apiKey": "xbAPupQKhkKvF176vE2JxAdPGpmWVu251Hldn6K4Z6Y"
},
"fileDefaultProvider": "LOCAL",
"fileProviders": {
"LOCAL": {
"path":"../uploads"
},
"AZURE": {
"account": "",
"accountKey": "",
"container": "",
"folder": ""
}
}
}
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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",
Expand All @@ -41,6 +42,7 @@
"koa-session": "^6.4.0",
"koa-static": "^5.0.0",
"meilisearch": "^0.37.0",
"mime-types": "^2.1.35",
"prom-client": "^15.0.0",
"superjson": "^2.2.1",
"trpc-koa-adapter": "^1.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "FileProvider" AS ENUM ('LOCAL', 'AZURE');

-- 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")
);

-- 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")
);

-- CreateIndex
CREATE UNIQUE INDEX "UnterveranstaltungDocument_fileId_key" ON "UnterveranstaltungDocument"("fileId");

-- CreateIndex
CREATE UNIQUE INDEX "File_id_key" ON "File"("id");

-- CreateIndex
CREATE UNIQUE INDEX "File_key_key" ON "File"("key");

-- 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;
144 changes: 86 additions & 58 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch"]
}

Expand Down Expand Up @@ -112,17 +112,17 @@ enum Konfektionsgroesse {
}

model Hostname {
id Int @id @default(autoincrement())
hostname String
id Int @id @default(autoincrement())
hostname String
veranstaltung Veranstaltung[]
}

model Notfallkontakt {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
firstname String
lastname String
telefon String
istErziehungsberechtigt Boolean @default(false)
istErziehungsberechtigt Boolean @default(false)
personId Int
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ model Person {
model Account {
id Int @id @default(autoincrement())
email String @unique
dlrgOauthId String? @unique
dlrgOauthId String? @unique
password String?
role Role
personId Int @unique
Expand Down Expand Up @@ -238,26 +238,26 @@ model Ort {
}

model Veranstaltung {
id Int @id @default(autoincrement())
name String
beginn DateTime @db.Date
ende DateTime @db.Date
meldebeginn DateTime
meldeschluss DateTime
ortId Int?
ort Ort? @relation(fields: [ortId], references: [id], onDelete: SetNull)
maxTeilnehmende Int
teilnahmegebuehr Int
unterveranstaltungen Unterveranstaltung[]
mahlzeiten Mahlzeit[]
beschreibung String?
datenschutz String?
teilnahmeBedingungen String?
teilnahmeBedingungenPublic String?
zielgruppe String?
hostnameId Int?
hostname Hostname? @relation(fields: [hostnameId], references: [id])
customFields CustomField[]
id Int @id @default(autoincrement())
name String
beginn DateTime @db.Date
ende DateTime @db.Date
meldebeginn DateTime
meldeschluss DateTime
ortId Int?
ort Ort? @relation(fields: [ortId], references: [id], onDelete: SetNull)
maxTeilnehmende Int
teilnahmegebuehr Int
unterveranstaltungen Unterveranstaltung[]
mahlzeiten Mahlzeit[]
beschreibung String?
datenschutz String?
teilnahmeBedingungen String?
teilnahmeBedingungenPublic String?
zielgruppe String?
hostnameId Int?
hostname Hostname? @relation(fields: [hostnameId], references: [id])
customFields CustomField[]
}

enum MahlzeitType {
Expand All @@ -281,20 +281,31 @@ enum UnterveranstaltungType {
}

model Unterveranstaltung {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
maxTeilnehmende Int
teilnahmegebuehr Int
meldebeginn DateTime
meldeschluss DateTime
veranstaltungId Int
veranstaltung Veranstaltung @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade)
veranstaltung Veranstaltung @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade)
gliederungId Int
gliederung Gliederung @relation(fields: [gliederungId], references: [id], onDelete: Cascade)
gliederung Gliederung @relation(fields: [gliederungId], references: [id], onDelete: Cascade)
Anmeldung Anmeldung[]
beschreibung String?
bedingungen String?
type UnterveranstaltungType @default(GLIEDERUNG)
type UnterveranstaltungType @default(GLIEDERUNG)
customFields CustomField[]
documents UnterveranstaltungDocument[]
}

model UnterveranstaltungDocument {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wozu diese extra Entität? Verknüpfung Unterveranstaltung -> File reicht in meinen Augen, dann als Array einfach

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Gedanke hier ist, dass Dokumente auch einen beschreibenden Namen enthalten kann. Theoretisch könnte man das auch in File packen.

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
}

enum ActivityType {
Expand All @@ -306,15 +317,15 @@ enum ActivityType {
}

model Activity {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
type ActivityType
description String?
subjectType String
subjectId Int?
causerId Int?
causer Account? @relation(fields: [causerId], references: [id], onDelete: SetNull)
metadata Json @default("{}")
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
type ActivityType
description String?
subjectType String
subjectId Int?
causerId Int?
causer Account? @relation(fields: [causerId], references: [id], onDelete: SetNull)
metadata Json @default("{}")
}

enum CustomFieldType {
Expand All @@ -335,26 +346,43 @@ enum CustomFieldPosition {
}

model CustomField {
id Int @id @default(autoincrement())
name String
description String?
type CustomFieldType
required Boolean @default(false)
options String[]
role Role[]
values CustomFieldValue[]
positions CustomFieldPosition[]
veranstaltungId Int?
unterveranstaltungId Int?
veranstaltung Veranstaltung? @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade)
unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
name String
description String?
type CustomFieldType
required Boolean @default(false)
options String[]
role Role[]
values CustomFieldValue[]
positions CustomFieldPosition[]
veranstaltungId Int?
unterveranstaltungId Int?
veranstaltung Veranstaltung? @relation(fields: [veranstaltungId], references: [id], onDelete: Cascade)
unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade)
}

model CustomFieldValue {
id Int @id @default(autoincrement())
value Json @default("{}")
fieldId Int
field CustomField @relation(fields: [fieldId], references: [id], onDelete: Cascade)
anmeldungId Int?
anmeldung Anmeldung? @relation(fields: [anmeldungId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
value Json @default("{}")
fieldId Int
field CustomField @relation(fields: [fieldId], references: [id], onDelete: Cascade)
anmeldungId Int?
anmeldung Anmeldung? @relation(fields: [anmeldungId], references: [id], onDelete: Cascade)
}

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()
supermomme marked this conversation as resolved.
Show resolved Hide resolved
filename String?
mimetype String?
UnterveranstaltungDocument UnterveranstaltungDocument?
}
30 changes: 30 additions & 0 deletions api/src/azureStorage.ts
Original file line number Diff line number Diff line change
@@ -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)
})
13 changes: 13 additions & 0 deletions api/src/config.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -43,6 +44,18 @@ 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(),
}),
}),
})

export default configSchema.parse(baseConfig)
Loading
Loading