diff --git a/docs/pages/installation.md b/docs/pages/installation.md index 7d88cb90..7fad538b 100644 --- a/docs/pages/installation.md +++ b/docs/pages/installation.md @@ -66,6 +66,8 @@ The following environment variables can be set if you want to enable specific fe | `Turnierplan__LogoUrl` | The URL of the custom logo to be displayed in the header of the public pages. If not specified, the turnierplan.NET logo will be shown instead. | - | | `Turnierplan__ImprintUrl` | The URL of your external imprint page if you want it to be linked on the public pages. | - | | `Turnierplan__PrivacyUrl` | The URL of your external privacy page if you want it to be linked on the public pages. | - | +| `Turnierplan__ImageMaxSize` | The maximum allowed file size when uploading an image file. The default value equates to 8 MiB (8 · 1024 · 1024) | `8388608` | +| `Turnierplan__ImageQuality` | Uploaded images are compressed using the `webp` format with the specified quality. A value of `100` will result in lossless compression being uesd. | `80` | !!! note The token lifetimes must be specified as .NET `TimeSpan` strings. For example `00:03:00` means 3 minutes or `1.00:00.00` means 1 day. diff --git a/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs b/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs index 56142d97..cf7f5495 100644 --- a/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs +++ b/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs @@ -46,7 +46,7 @@ public void IsActionAllowed___When_Called_With_Indirect_Target___Returns_Expecte organization.AddRoleAssignment(Role.Contributor, otherPrincipal); Test(() => new ApiKey(organization, "Test", null, DateTime.MaxValue)); - Test(() => new Image(organization, "Test", ImageType.Logo, "", 0, 1, 1)); + Test(() => new Image(organization, "Test", "", 0, 1, 1)); Test(() => new Folder(organization, "Test")); Test(() => new Tournament(organization, "Test", Visibility.Public)); Test(() => new Venue(organization, "Test", "")); @@ -121,7 +121,7 @@ public void AddAvailableRoles___When_Called_With_Indirect_Target___Returns_Expec organization.AddRoleAssignment(Role.Contributor, otherPrincipal); Test(() => new ApiKey(organization, "Test", null, DateTime.MaxValue)); - Test(() => new Image(organization, "Test", ImageType.Logo, "", 0, 1, 1)); + Test(() => new Image(organization, "Test", "", 0, 1, 1)); Test(() => new Folder(organization, "Test")); Test(() => new Tournament(organization, "Test", Visibility.Public)); Test(() => new Venue(organization, "Test", "")); @@ -260,12 +260,10 @@ public void AddRolesToResponseHeader___When_Called_Such_That_Entities_Are_Proces var headers = httpContextAccessor.HttpContext!.Response.Headers; var headerValues = headers["X-Turnierplan-Roles"]; - var organizationId = organization.PublicId.ToString(); var tournamentId1 = tournament1.PublicId.ToString(); var tournamentId2 = tournament2.PublicId.ToString(); - headerValues.Should().HaveCount(3); - headerValues.Single(x => x!.StartsWith(organizationId)).Should().Be($"{organizationId}=Reader"); + headerValues.Should().HaveCount(2); headerValues.Single(x => x!.StartsWith(tournamentId1)).Should().Be($"{tournamentId1}=Owner+Reader+Contributor"); headerValues.Single(x => x!.StartsWith(tournamentId2)).Should().Be($"{tournamentId2}=Reader+Contributor"); } diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index 49d6da23..e7e70a71 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -170,6 +170,7 @@ export const de = { Tournaments: 'Turniere', Venues: 'Spielstätten', PlanningRealms: 'Turnierplaner', + Images: 'Bilder', ApiKeys: 'API-Schlüssel', Settings: 'Einstellungen' }, @@ -177,11 +178,14 @@ export const de = { TournamentCount: 'Turniere', VenueCount: 'Spielstätten', PlanningRealmCount: 'Turnierplaner', + ImagesCount: 'Bilder', + ImagesTotalSize: 'Gesamtgröße', ApiKeyCount: 'API-Schlüssel' }, NewTournament: 'Neues Turnier', NewVenue: 'Neue Spielstätte', NewPlanningRealm: 'Neuer Turnierplaner', + UploadImage: 'Bild hochladen', NewApiKey: 'Neuer API-Schlüssel', NoTournaments: 'In dieser Organisation gibt es aktuell keine Turniere.\nErstellen Sie ein Turner mit der Schaltfläche oben rechts.', NoVenues: @@ -211,6 +215,26 @@ export const de = { NoDescription: 'Keine Beschreibung vorhanden', Open: 'öffnen' }, + Images: { + Preview: 'Vorschau', + Dimensions: '{{w}} x {{h}} px', + CreatedAt: 'Hochgeladen am', + Name: 'Name', + References: { + Header: 'Verwendungen', + Tooltip: 'Gibt an, von wie vielen Turnieren wird dieses Bild verwendet wird', + None: 'keine' + }, + NoImages: 'Es sind aktuell keine Bilder vorhanden.', + Rename: { + Title: 'Bild umbenennen', + EnterNewName: 'Geben Sie den neuen Namen für das Bild ein:' + }, + DeleteToast: { + Title: 'Bild wurde gelöscht', + Message: 'Das Bild wurde gelöscht.' + } + }, ApiKeys: { TableLabel: 'API Schlüssel dieser Organisation', Id: 'ID', @@ -598,7 +622,9 @@ export const de = { } }, EditImages: { - Title: 'Logos & Bilddateien' + Title: 'Logos & Bilddateien', + BannerAspectRatioWarning: + 'Das aktuelle Banner hat eine Auflösung von {{w}} x {{h}} px und ein Seitenverhältnis von {{ratio}}:1. Für eine optimale Darstellung verwenden Sie ein Bild mit einem Seitenverhältnis von mindestens 3:1' }, MoveToAnotherFolder: { Title: 'Turnier verschieben', @@ -1199,6 +1225,20 @@ export const de = { } } }, + UploadImage: { + Title: 'Bild hochladen', + Form: { + File: 'Datei auswählen:', + Name: 'Name:', + NameInvalid: 'Der angegebene Name ist ungültig' + }, + NameTooltip: 'Wenn der Name leergelassen wird, wird der Dateinahme der gewählten Datei als Name verwendet.', + Preview: 'Bildvorschau:', + PreviewAlt: 'Bildvorschau der Datei "{{fileName}}"', + OrganizationNotice: + 'Es wird ein neues Bild in der Organisation {{organizationName}} hochgeladen. Das Bild kann anschließend von allen Turnieren und Turnierplanern innerhalb der Organisation verwendet werden.', + Submit: 'Hochladen' + }, CreateApiKey: { Title: 'Neuen API-Schlüssel erstellen', Form: { @@ -1260,23 +1300,11 @@ export const de = { Banner: 'Banner' }, ImageChooser: { - Title: 'Bild hochladen oder auswählen', + Title: 'Bild auswählen', Remove: 'Bild entfernen', - NoImages: 'Laden Sie Ihr erstes Bild hoch...', - Upload: 'Hochladen', - UploadFailed: 'Das Bild konnte nicht hochgeladen werden. Prüfen Sie die Maße und die maximale Dateigröße.', - Constraints: { - Logo: 'Das Bild muss quadratisch sein mit einer Auflösung zwischen 50x50 und 3000x3000 Pixel. Die maximale Dateigröße beträgt 8 MB.', - Banner: - 'Das Bild muss mindestens 50px hoch sein und darf maximal 3000px breit sein. Das Seitenverhältnis muss zwischen 3:1 und 5:1 liegen. Die maximale Dateigröße beträgt 8 MB.' - }, - DetailView: { - Title: 'Hier sehen Sie die Detailinformationen zu folgendem Bild:', - Name: 'Dateiname: {{value}}', - CreatedAt: 'Hochgeladen am: {{value}}', - FileSize: 'Dateigröße: {{value}} KB', - Resolution: 'Auflösung: {{width}}x{{height}} px' - } + NoImages: 'In dieser Organisation wurden bisher noch keine Bilder hochgeladen.', + UploadViaOrgPage: 'Neue Bilder können Sie auf der Seite der Organisation hochladen.', + Dimensions: '{{w}} x {{h}} px' }, MultiSelectFilter: { All: 'alle', @@ -1352,6 +1380,10 @@ export const de = { Tooltip: 'Ordner', NotInherited: 'Zuweisung liegt auf diesem Ordner' }, + Image: { + Tooltip: 'Bild', + NotInherited: 'Zuweisung liegt auf diesem Bild' + }, Organization: { Tooltip: 'Organisation', NotInherited: 'Zuweisung liegt auf dieser Organisation' diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.html b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.html index 7505ffbd..698a7ac6 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.html @@ -8,52 +8,14 @@
- } @else if (isInImageDetailView) { -
- - -
- -
- -
-
-
-
-
-
-
- -
-
} @else {
- +
- @if (isUploadingImage) { - - } @else { - - }
- @if (hasUploadError) { - - } -
@for (image of existingImages; track image.id) {
- @if (image.id !== currentImageId) { } + {{ image.name }} + + {{ image.fileSize | fileSize: 'de' }}
- +
} @empty { @@ -91,17 +53,13 @@
@if (!isLoadingImages) { } } diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.scss b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.scss index 53ac73ed..38440487 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.scss +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.scss @@ -2,13 +2,21 @@ max-height: 40em; } +.image-info { + max-width: 9em; + + &.xsmall { + font-size: 0.7em; + } +} + .image-tile { &:hover, &.hover-override { border-color: #333 !important; .hover-buttons { - background: rgb(255 255 255 / 60%); + background: rgb(255 255 255 / 85%); display: flex !important; } } diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.ts index 8f46d38a..e633dee7 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-chooser/image-chooser.component.ts @@ -4,23 +4,15 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Actions } from '../../../generated/actions'; import { TranslateDirective } from '@ngx-translate/core'; import { LoadingIndicatorComponent } from '../loading-indicator/loading-indicator.component'; -import { IsActionAllowedDirective } from '../../directives/is-action-allowed.directive'; -import { DeleteButtonComponent } from '../delete-button/delete-button.component'; -import { SmallSpinnerComponent } from '../../../core/components/small-spinner/small-spinner.component'; import { ActionButtonComponent } from '../action-button/action-button.component'; -import { AlertComponent } from '../alert/alert.component'; import { NgClass } from '@angular/common'; -import { TranslateDatePipe } from '../../pipes/translate-date.pipe'; import { TurnierplanApi } from '../../../api/turnierplan-api'; -import { ImageType } from '../../../api/models/image-type'; -import { uploadImage$FormData } from '../../../api/fn/images/upload-image-form-data'; import { getImages } from '../../../api/fn/images/get-images'; import { ImageDto } from '../../../api/models/image-dto'; -import { deleteImage } from '../../../api/fn/images/delete-image'; -import { from, switchMap } from 'rxjs'; +import { FileSizePipe } from '../../pipes/file-size.pipe'; export interface ImageChooserResult { - type: 'ImageDeleted' | 'ImageSelected' | 'ImageUploaded'; + type: 'ImageDeleted' | 'ImageSelected'; image?: ImageDto; deletedImageId?: string; } @@ -28,140 +20,49 @@ export interface ImageChooserResult { @Component({ templateUrl: './image-chooser.component.html', styleUrls: ['./image-chooser.component.scss'], - imports: [ - TranslateDirective, - LoadingIndicatorComponent, - IsActionAllowedDirective, - DeleteButtonComponent, - SmallSpinnerComponent, - ActionButtonComponent, - AlertComponent, - NgClass, - TranslateDatePipe - ] + imports: [TranslateDirective, LoadingIndicatorComponent, ActionButtonComponent, NgClass, FileSizePipe] }) export class ImageChooserComponent { protected readonly Actions = Actions; protected isInitialized = false; - protected organizationId!: string; - protected imageType!: ImageType; - protected existingImages: ImageDto[] = []; protected isLoadingImages = true; protected currentImageId?: string; - protected isUploadingImage = false; - protected hasUploadError = false; - // On mobile, where hovering does not work, the current "hovered" image is set by clicking and stored in this variable. protected hoverOverrideImageId?: string; - protected imageForDetailView?: ImageDto; - constructor( protected readonly modal: NgbActiveModal, private readonly turnierplanApi: TurnierplanApi ) {} - protected get isInImageDetailView(): boolean { - return this.imageForDetailView !== undefined; - } - - public init(organizationId: string, imageType: ImageType, currentImageId?: string): void { + public init(organizationId: string, currentImageId?: string): void { if (this.isInitialized) { return; } this.organizationId = organizationId; - this.imageType = imageType; this.currentImageId = currentImageId; this.isInitialized = true; this.loadImages(); } - protected openFileSelectionDialog(): void { - if (this.isUploadingImage) { - return; - } - - const tempElement = document.createElement('input'); - tempElement.type = 'file'; - tempElement.accept = 'image/png,image/jpeg'; - - tempElement.addEventListener('change', (event) => { - const targetFile = (event.target as HTMLInputElement).files?.item(0); - - if (targetFile) { - this.isUploadingImage = true; - - from(targetFile.arrayBuffer()) - .pipe( - switchMap((arrayBuffer) => - this.turnierplanApi.invoke(uploadImage$FormData, { - body: { - organizationId: this.organizationId, - imageType: this.imageType, - imageName: targetFile.name, - image: new Blob([arrayBuffer]) - } - }) - ) - ) - .subscribe({ - next: (result) => { - this.modal.close({ type: 'ImageUploaded', image: result } as ImageChooserResult); - }, - error: () => { - this.isUploadingImage = false; - this.hasUploadError = true; - } - }); - } - }); - - tempElement.click(); - } - - protected getRoundedFileSize(fileSize: number): number { - return Math.round(fileSize / 1000); - } - - protected deleteCurrentViewedImage(): void { - if (!this.imageForDetailView) { - return; - } - - const deleteImageId = this.imageForDetailView.id; - this.imageForDetailView = undefined; - - this.isLoadingImages = true; - - this.turnierplanApi.invoke(deleteImage, { id: deleteImageId }).subscribe({ - next: () => { - if (deleteImageId === this.currentImageId) { - this.modal.close({ type: 'ImageDeleted', deletedImageId: deleteImageId } as ImageChooserResult); - } else { - this.loadImages(); + private loadImages(): void { + this.turnierplanApi.invoke(getImages, { organizationId: this.organizationId }).subscribe({ + next: (response) => { + this.existingImages = response.images; + + // Keep the original sorting from the response, but always move the current image to the first position + const currentImageIndex = this.existingImages.findIndex((x) => x.id === this.currentImageId); + if (currentImageIndex !== -1) { + const currentImage = this.existingImages.splice(currentImageIndex, 1); + this.existingImages = [currentImage[0], ...this.existingImages]; } - }, - error: (error) => { - this.modal.dismiss({ isApiError: true, apiError: error as unknown }); - } - }); - } - private loadImages(): void { - this.turnierplanApi.invoke(getImages, { organizationId: this.organizationId, imageType: this.imageType }).subscribe({ - next: (images) => { - this.existingImages = images; - this.existingImages.sort((a, b) => { - if (a.id === this.currentImageId) return -1; - if (b.id === this.currentImageId) return 1; - return new Date(b.createdAt).getDate() - new Date(a.createdAt).getDate(); - }); this.isLoadingImages = false; }, error: (error) => { diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.html b/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.html new file mode 100644 index 00000000..8069ec88 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.html @@ -0,0 +1,72 @@ +
+ + + + + + + + + + + + + @for (image of _images; track image.id) { + @let writeAllowed = (authorizationService.isActionAllowed$(image.id, Actions.GenericWrite) | async) ?? false; + + + + + + + + + + + } @empty { + + + + } + +
+ + +
+ +
+
{{ image.fileSize | fileSize: 'de' }}
+
{{ image.createdAt | translateDate }} + @if (image.isUpdatingName) { + + } @else if (writeAllowed) { + + } + + {{ image.name }} + + @if (image.referenceCount === 0) { + + } @else if (image.referenceCount !== undefined) { + {{ image.referenceCount }} + } + + + + @if (writeAllowed) { + + } +
+
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.ts new file mode 100644 index 00000000..aca5694a --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-manager/image-manager.component.ts @@ -0,0 +1,99 @@ +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { ImageDto } from '../../../api/models/image-dto'; +import { TranslateDirective } from '@ngx-translate/core'; +import { AsyncPipe } from '@angular/common'; +import { Actions } from '../../../generated/actions'; +import { AuthorizationService } from '../../../core/services/authorization.service'; +import { RenameButtonComponent } from '../rename-button/rename-button.component'; +import { TurnierplanApi } from '../../../api/turnierplan-api'; +import { SmallSpinnerComponent } from '../../../core/components/small-spinner/small-spinner.component'; +import { setImageName } from '../../../api/fn/images/set-image-name'; +import { IsActionAllowedDirective } from '../../directives/is-action-allowed.directive'; +import { RbacWidgetComponent } from '../rbac-widget/rbac-widget.component'; +import { DeleteButtonComponent } from '../delete-button/delete-button.component'; +import { deleteImage } from '../../../api/fn/images/delete-image'; +import { NotificationService } from '../../../core/services/notification.service'; +import { TooltipIconComponent } from '../tooltip-icon/tooltip-icon.component'; +import { TranslateDatePipe } from '../../pipes/translate-date.pipe'; +import { FileSizePipe } from '../../pipes/file-size.pipe'; + +type ImageView = ImageDto & { referenceCount?: number; isUpdatingName: boolean }; + +@Component({ + selector: 'tp-image-manager', + imports: [ + TranslateDirective, + AsyncPipe, + RenameButtonComponent, + SmallSpinnerComponent, + IsActionAllowedDirective, + RbacWidgetComponent, + DeleteButtonComponent, + TooltipIconComponent, + TranslateDatePipe, + FileSizePipe + ], + templateUrl: './image-manager.component.html' +}) +export class ImageManagerComponent { + @Input({ required: true }) + public set images(value: { images: ImageDto[]; references?: { [key: string]: number } }) { + this._images = value.images.map((x) => ({ + ...x, + referenceCount: value.references ? value.references[x.id] : undefined, + isUpdatingName: false + })); + } + + @Output() + public errorOccured = new EventEmitter(); + + protected readonly authorizationService = inject(AuthorizationService); + protected readonly Actions = Actions; + + protected _images: ImageView[] = []; + + private readonly turnierplanApi = inject(TurnierplanApi); + private readonly notificationService = inject(NotificationService); + + protected renameImage(id: string, name: string): void { + if (this._images.some((x) => x.isUpdatingName)) { + return; + } + + const image = this._images.find((x) => x.id === id); + + if (!image) { + return; + } + + image.isUpdatingName = true; + + this.turnierplanApi.invoke(setImageName, { id: id, body: { name: name } }).subscribe({ + next: () => { + image.isUpdatingName = false; + image.name = name; + }, + error: (error) => { + this.errorOccured.emit(error); + } + }); + } + + protected deleteImage(id: string): void { + this.turnierplanApi.invoke(deleteImage, { id: id }).subscribe({ + next: () => { + this._images = this._images.filter((x) => x.id !== id); + + this.notificationService.showNotification( + 'info', + 'Portal.ViewOrganization.Images.DeleteToast.Title', + 'Portal.ViewOrganization.Images.DeleteToast.Message' + ); + }, + error: (error) => { + this.errorOccured.emit(error); + } + }); + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/components/image-widget/image-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/image-widget/image-widget.component.ts index 553bd42e..7fa979b9 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/image-widget/image-widget.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/image-widget/image-widget.component.ts @@ -5,7 +5,6 @@ import { ImageChooserComponent, ImageChooserResult } from '../image-chooser/imag import { TranslateDirective } from '@ngx-translate/core'; import { ActionButtonComponent } from '../action-button/action-button.component'; import { ImageDto } from '../../../api/models/image-dto'; -import { ImageType } from '../../../api/models/image-type'; @Component({ selector: 'tp-image-widget', @@ -19,9 +18,6 @@ export class ImageWidgetComponent { @Input() public imageAlt!: string; - @Input() - public imageType!: ImageType; - @Input() public organizationId!: string; @@ -51,11 +47,11 @@ export class ImageWidgetComponent { }); const component = ref.componentInstance as ImageChooserComponent; - component.init(this.organizationId, this.imageType, this.currentImage?.id); + component.init(this.organizationId, this.currentImage?.id); ref.closed.subscribe({ next: (result: ImageChooserResult) => { - if (result.type === 'ImageUploaded' || result.type === 'ImageSelected') { + if (result.type === 'ImageSelected') { this.imageChange.emit(result.image); } else if (result.type === 'ImageDeleted') { this.imageDelete.emit(result.deletedImageId); diff --git a/src/Turnierplan.App/Client/src/app/portal/components/invitation-link-tile/invitation-link-tile.component.html b/src/Turnierplan.App/Client/src/app/portal/components/invitation-link-tile/invitation-link-tile.component.html index 27bf053b..7d19e2c5 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/invitation-link-tile/invitation-link-tile.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/invitation-link-tile/invitation-link-tile.component.html @@ -84,7 +84,6 @@ (); protected readonly Actions = Actions; - protected readonly ImageType = ImageType; protected invitationLinkExpired = false; protected tournamentClassesToAdd: TournamentClassDto[] = []; diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index f569dad6..98cf18cd 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -71,6 +71,9 @@ export class RbacOffcanvasComponent implements OnDestroy { case 'Folder': this.targetIcon = 'folder2-open'; break; + case 'Image': + this.targetIcon = 'image'; + break; case 'Organization': this.targetIcon = 'boxes'; break; diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.html new file mode 100644 index 00000000..d94825a5 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.html @@ -0,0 +1,57 @@ + + @let imageNameInvalid = (imageName.dirty || imageName.touched) && imageName.value.length > 0 && imageName.value.trim().length === 0; + + @if (uploadStarted) { + + } @else { +
+ + +
+ +
+
+ + +
+ +
+
+ } + + @let previewUrl = previewUrl$ | async; + @if (previewUrl) { +
+
+
+ +
+ } + +
+ +
+ + +
+ +
+ +
+ +
+
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.ts new file mode 100644 index 00000000..c9f674cb --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/upload-image/upload-image.component.ts @@ -0,0 +1,164 @@ +import { Component, inject, OnDestroy } from '@angular/core'; +import { LoadingState, LoadingStateDirective } from '../../directives/loading-state.directive'; +import { PageFrameComponent } from '../../components/page-frame/page-frame.component'; +import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { OrganizationDto } from '../../../api/models/organization-dto'; +import { TooltipIconComponent } from '../../components/tooltip-icon/tooltip-icon.component'; +import { BehaviorSubject, map, Observable, of, switchMap } from 'rxjs'; +import { AsyncPipe } from '@angular/common'; +import { filter } from 'rxjs/operators'; +import { ActionButtonComponent } from '../../components/action-button/action-button.component'; +import { LoadingIndicatorComponent } from '../../components/loading-indicator/loading-indicator.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { getOrganization } from '../../../api/fn/organizations/get-organization'; +import { TurnierplanApi } from '../../../api/turnierplan-api'; +import { TitleService } from '../../services/title.service'; +import { uploadImage$FormData } from '../../../api/fn/images/upload-image-form-data'; +import { LocalStorageService } from '../../services/local-storage.service'; +import { ViewOrganizationComponent } from '../view-organization/view-organization.component'; + +@Component({ + imports: [ + LoadingStateDirective, + PageFrameComponent, + TranslatePipe, + TranslateDirective, + ReactiveFormsModule, + TooltipIconComponent, + AsyncPipe, + ActionButtonComponent, + LoadingIndicatorComponent + ], + templateUrl: './upload-image.component.html' +}) +export class UploadImageComponent implements OnDestroy { + protected loadingState: LoadingState = { isLoading: false }; + + protected organization?: OrganizationDto; + protected imageName = new FormControl('', { nonNullable: true }); + protected selectedFile?: File; + protected fallbackName?: string; + protected previewUrl$: Observable; + protected uploadStarted = false; + + private readonly localStorageService = inject(LocalStorageService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly titleService = inject(TitleService); + private readonly turnierplanApi = inject(TurnierplanApi); + + private blob$: BehaviorSubject; + + constructor() { + this.blob$ = new BehaviorSubject(undefined); + this.previewUrl$ = this.blob$.pipe( + filter((blob) => !!blob), + map((blob) => URL.createObjectURL(blob)) + ); + + this.route.paramMap + .pipe( + takeUntilDestroyed(), + switchMap((params) => { + const organizationId = params.get('id'); + if (organizationId === null) { + this.loadingState = { isLoading: false }; + return of(undefined); + } + + this.loadingState = { isLoading: true }; + return this.turnierplanApi.invoke(getOrganization, { id: organizationId }); + }) + ) + .subscribe({ + next: (organization) => { + this.organization = organization; + this.titleService.setTitleFrom(organization); + this.loadingState = { isLoading: false }; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + + public ngOnDestroy() { + this.blob$.complete(); + } + + protected onFileSelected(event: unknown): void { + if (this.uploadStarted) { + return; + } + + const file = (event as { target: HTMLInputElement }).target.files?.item(0); + + if (!file) { + return; + } + + this.selectedFile = file; + this.setFallbackFileName(file.name); + + this.selectedFile.arrayBuffer().then((arrayBuffer): void => { + this.blob$.next(new Blob([arrayBuffer])); + }); + } + + protected confirmButtonClicked(): void { + const currentBlob = this.blob$.value; + const organizationId = this.organization?.id; + + if (this.uploadStarted || !currentBlob || !organizationId) { + return; + } + + this.uploadStarted = true; + + let imageName = this.imageName.value.trim(); + if (imageName.length === 0) { + imageName = this.fallbackName ?? '-'; + } + + this.turnierplanApi + .invoke(uploadImage$FormData, { + body: { + organizationId: organizationId, + imageName: imageName, + image: currentBlob + } + }) + .subscribe({ + next: (): void => { + this.localStorageService.setNavigationTab(organizationId, ViewOrganizationComponent.imagesPageId); + void this.router.navigate(['../../'], { relativeTo: this.route }); + }, + error: (error): void => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + + private setFallbackFileName(name: string): void { + // Cut off the file extension + const i = name.lastIndexOf('.'); + + if (i === -1) { + this.fallbackName = name.trim(); + } else { + const extensions = name.substring(i); + if (extensions === '.png' || extensions === '.jpg' || extensions === '.jpg') { + this.fallbackName = name.substring(0, i).trim(); + } else { + this.fallbackName = name.trim(); + } + } + + if (this.fallbackName.length === 0) { + // Don't bother with translating this string because realistically, this will never happen + this.fallbackName = 'Unnamed Image'; + } + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html index 0a7a0df9..d79f4e34 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html @@ -34,6 +34,14 @@ [icon]="'plus-circle'" [routerLink]="'create/planning-realm'" /> } + @case (5) { + + } @case (2) { } } + @case (5) { + @if (isLoadingImages || !images) { + + } @else { +
+ + @if (imagesTotalSize !== undefined) { + + } +
+
+ +
+ } + } @case (2) { @if (isLoadingApiKeys || !apiKeys) { diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts index 3b99e2ba..41634a98 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts @@ -42,6 +42,10 @@ import { deleteApiKey } from '../../../api/fn/api-keys/delete-api-key'; import { setApiKeyStatus } from '../../../api/fn/api-keys/set-api-key-status'; import { getApiKeys } from '../../../api/fn/api-keys/get-api-keys'; import { E2eDirective } from '../../../core/directives/e2e.directive'; +import { ImageManagerComponent } from '../../components/image-manager/image-manager.component'; +import { getImages } from '../../../api/fn/images/get-images'; +import { GetImagesEndpointResponse } from '../../../api/models/get-images-endpoint-response'; +import { FileSizePipe } from '../../pipes/file-size.pipe'; @Component({ templateUrl: './view-organization.component.html', @@ -69,10 +73,14 @@ import { E2eDirective } from '../../../core/directives/e2e.directive'; TranslatePipe, TranslateDatePipe, IdWidgetComponent, - E2eDirective + E2eDirective, + ImageManagerComponent, + FileSizePipe ] }) export class ViewOrganizationComponent implements OnInit, OnDestroy { + public static readonly imagesPageId = 5; + private static readonly venuesPageId = 1; private static readonly apiKeysPageId = 2; private static readonly planningRealmsPageId = 4; @@ -84,11 +92,14 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { protected tournaments?: TournamentHeaderDto[]; protected venues?: VenueDto[]; protected planningRealms?: PlanningRealmHeaderDto[]; + protected images?: GetImagesEndpointResponse; + protected imagesTotalSize?: number; protected apiKeys?: ApiKeyDto[]; protected displayApiKeyUsage?: string; protected isLoadingVenues = false; - protected isLoadingApiKeys = false; protected isLoadingPlanningRealms = false; + protected isLoadingImages = false; + protected isLoadingApiKeys = false; protected isUpdatingName = false; @@ -109,6 +120,11 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { title: 'Portal.ViewOrganization.Pages.PlanningRealms', icon: 'bi-ticket-perforated' }, + { + id: ViewOrganizationComponent.imagesPageId, + title: 'Portal.ViewOrganization.Pages.Images', + icon: 'bi-image' + }, { id: ViewOrganizationComponent.apiKeysPageId, title: 'Portal.ViewOrganization.Pages.ApiKeys', @@ -209,6 +225,21 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { }); } + if (number === ViewOrganizationComponent.imagesPageId && !this.images && !this.isLoadingImages) { + // Load images only when the page is opened + this.isLoadingImages = true; + this.turnierplanApi.invoke(getImages, { organizationId: this.organization.id, includeReferences: true }).subscribe({ + next: (images) => { + this.images = images; + this.imagesTotalSize = images.images.reduce((sum, img) => sum + img.fileSize, 0); + this.isLoadingImages = false; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + if (number === ViewOrganizationComponent.apiKeysPageId && !this.apiKeys && !this.isLoadingApiKeys) { // Load API keys only when the page is opened this.loadApiKeys().subscribe({ diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.html index c37bbdb8..127c46c3 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.html @@ -321,7 +321,6 @@ class="flex-md-grow-1" [currentImage]="images.primaryLogo" [imageAlt]="'PrimaryLogo'" - [imageType]="ImageType.Logo" [organizationId]="tournament.organizationId" (imageChange)="setImage('primaryLogo', $event?.id)" (imageDelete)="loadTournamentImages()" @@ -330,7 +329,6 @@ class="flex-md-grow-1" [currentImage]="images.secondaryLogo" [imageAlt]="'SecondaryLogo'" - [imageType]="ImageType.Logo" [organizationId]="tournament.organizationId" (imageChange)="setImage('secondaryLogo', $event?.id)" (imageDelete)="loadTournamentImages()" @@ -339,12 +337,27 @@ class="flex-md-grow-1" [currentImage]="images.bannerImage" [imageAlt]="'Banner'" - [imageType]="ImageType.Banner" [organizationId]="tournament.organizationId" (imageChange)="setImage('bannerImage', $event?.id)" (imageDelete)="loadTournamentImages()" (apiError)="loadingState = { isLoading: false, error: $event }" /> + + @if (images.bannerImage) { + @let bannerAspectRatio = images.bannerImage.width / images.bannerImage.height; + @if (bannerAspectRatio < 3) { + + } + } }
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts index 135f4d39..0ed0cfa9 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts @@ -26,7 +26,7 @@ import { ActionButtonComponent } from '../../components/action-button/action-but import { IsActionAllowedDirective } from '../../directives/is-action-allowed.directive'; import { SmallSpinnerComponent } from '../../../core/components/small-spinner/small-spinner.component'; import { RenameButtonComponent } from '../../components/rename-button/rename-button.component'; -import { AsyncPipe, NgClass } from '@angular/common'; +import { AsyncPipe, DecimalPipe, NgClass } from '@angular/common'; import { BadgeComponent } from '../../components/badge/badge.component'; import { FormsModule } from '@angular/forms'; import { LoadingIndicatorComponent } from '../../components/loading-indicator/loading-indicator.component'; @@ -42,7 +42,6 @@ import { MatchTreeComponent } from '../../components/match-tree/match-tree.compo import { TranslateDatePipe } from '../../pipes/translate-date.pipe'; import { IdWidgetComponent } from '../../components/id-widget/id-widget.component'; import { TurnierplanApi } from '../../../api/turnierplan-api'; -import { ImageType } from '../../../api/models/image-type'; import { Visibility } from '../../../api/models/visibility'; import { TournamentDto } from '../../../api/models/tournament-dto'; import { TournamentImagesDto } from '../../../api/models/tournament-images-dto'; @@ -109,7 +108,8 @@ import { deleteRankingOverwrite } from '../../../api/fn/ranking-overwrites/delet AsyncPipe, TranslatePipe, TranslateDatePipe, - IdWidgetComponent + IdWidgetComponent, + DecimalPipe ] }) export class ViewTournamentComponent implements OnInit, OnDestroy { @@ -118,7 +118,6 @@ export class ViewTournamentComponent implements OnInit, OnDestroy { private static readonly DocumentsPageId = 4; private static readonly SettingsPageId = 6; - protected readonly ImageType = ImageType; protected readonly Visibility = Visibility; protected readonly Actions = Actions; diff --git a/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.spec.ts b/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.spec.ts new file mode 100644 index 00000000..ef3656dd --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.spec.ts @@ -0,0 +1,20 @@ +import { FileSizePipe } from './file-size.pipe'; + +describe('FileSizePipe', () => { + it('formats values as expected', () => { + const pipe = new FileSizePipe(); + + expect(pipe.transform(189, 'de')).toBe('189 B'); + expect(pipe.transform(999, 'de')).toBe('999 B'); + expect(pipe.transform(1000, 'de')).toBe('1,0 kB'); + expect(pipe.transform(1495, 'de')).toBe('1,5 kB'); + expect(pipe.transform(1999, 'de')).toBe('2,0 kB'); + expect(pipe.transform(95_456, 'de')).toBe('95,5 kB'); + expect(pipe.transform(822_300, 'de')).toBe('822,3 kB'); + expect(pipe.transform(1_456_488, 'de')).toBe('1,5 MB'); + expect(pipe.transform(141_300_000, 'de')).toBe('141,3 MB'); + expect(pipe.transform(1_000_000_000, 'de')).toBe('1,0 GB'); + expect(pipe.transform(195_000_000_000, 'de')).toBe('195,0 GB'); + expect(pipe.transform(1_000_000_000_000, 'de')).toBe('1.000,0 GB'); + }); +}); diff --git a/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.ts b/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.ts new file mode 100644 index 00000000..6809f39f --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pipes/file-size.pipe.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { formatNumber } from '@angular/common'; + +@Pipe({ + name: 'fileSize' +}) +export class FileSizePipe implements PipeTransform { + public transform(value: number, locale: string): string { + let suffix = ''; + let digitsFormat = '1.1-1'; + + if (value >= 1_000_000_000) { + value /= 1_000_000_000; + suffix = 'GB'; + } else if (value >= 1_000_000) { + value /= 1_000_000; + suffix = 'MB'; + } else if (value >= 1_000) { + value /= 1_000; + suffix = 'kB'; + } else { + suffix = 'B'; + digitsFormat = '1.0'; + } + + return formatNumber(value, locale, digitsFormat) + ' ' + suffix; + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts index 97d16a65..df381dfd 100644 --- a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts +++ b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts @@ -18,6 +18,7 @@ import { ViewTournamentComponent } from './pages/view-tournament/view-tournament import { ConfigureTournamentComponent } from './pages/configure-tournament/configure-tournament.component'; import { EditMatchPlanComponent } from './pages/edit-match-plan/edit-match-plan.component'; import { ViewVenueComponent } from './pages/view-venue/view-venue.component'; +import { UploadImageComponent } from './pages/upload-image/upload-image.component'; export const portalRoutes: Routes = [ { @@ -50,6 +51,10 @@ export const portalRoutes: Routes = [ path: 'organization/:id/create/api-key', component: CreateApiKeyComponent }, + { + path: 'organization/:id/upload/image', + component: UploadImageComponent + }, { path: 'organization/:id/create/planning-realm', component: CreatePlanningRealmComponent diff --git a/src/Turnierplan.App/Client/src/test.ts b/src/Turnierplan.App/Client/src/test.ts index 09230c64..a8c4d933 100644 --- a/src/Turnierplan.App/Client/src/test.ts +++ b/src/Turnierplan.App/Client/src/test.ts @@ -3,6 +3,11 @@ import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; +import localeDeExtra from '@angular/common/locales/extra/de'; + +registerLocaleData(localeDe, 'de', localeDeExtra); // Initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), { diff --git a/src/Turnierplan.App/Converters/JsonPublicIdConverter.cs b/src/Turnierplan.App/Converters/JsonPublicIdConverter.cs index 1dcfb2f3..aeb9cccf 100644 --- a/src/Turnierplan.App/Converters/JsonPublicIdConverter.cs +++ b/src/Turnierplan.App/Converters/JsonPublicIdConverter.cs @@ -24,5 +24,13 @@ public override void Write(Utf8JsonWriter writer, PublicId value, JsonSerializer { writer.WriteStringValue(value.ToString()); } + + /// + /// Required for serialization of properties with the type Dictionary<PublicId, T> + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, PublicId value, JsonSerializerOptions options) + { + writer.WritePropertyName(value.ToString()); + } } diff --git a/src/Turnierplan.App/Endpoints/Images/GetImagesEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/GetImagesEndpoint.cs index 10339f4f..86655ac2 100644 --- a/src/Turnierplan.App/Endpoints/Images/GetImagesEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/GetImagesEndpoint.cs @@ -2,13 +2,12 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; -using Turnierplan.Core.Image; using Turnierplan.Core.PublicId; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.Images; -internal sealed class GetImagesEndpoint : EndpointBase> +internal sealed class GetImagesEndpoint : EndpointBase { protected override HttpMethod Method => HttpMethod.Get; @@ -18,12 +17,13 @@ internal sealed class GetImagesEndpoint : EndpointBase> private static async Task Handle( [FromQuery] PublicId organizationId, - [FromQuery] ImageType imageType, - IOrganizationRepository repository, + [FromQuery] bool? includeReferences, + IOrganizationRepository organizationRepository, + IImageRepository imageRepository, IAccessValidator accessValidator, IMapper mapper) { - var organization = await repository.GetByPublicIdAsync(organizationId, IOrganizationRepository.Includes.Images); + var organization = await organizationRepository.GetByPublicIdAsync(organizationId, IOrganizationRepository.Includes.Images); if (organization is null) { @@ -35,13 +35,39 @@ private static async Task Handle( return Results.Forbid(); } - var filteredImages = organization.Images.Where(x => x.Type == imageType).ToList(); + var sortedImages = organization.Images + .OrderByDescending(x => x.CreatedAt) + .ToList(); - foreach (var image in filteredImages) + foreach (var image in sortedImages) { accessValidator.AddRolesToResponseHeader(image); } - return Results.Ok(mapper.MapCollection(filteredImages)); + Dictionary? references = null; + + if (includeReferences == true) + { + references = []; + + foreach (var image in sortedImages) + { + var count = await imageRepository.CountNumberOfReferencingTournamentsAsync(image.Id); + references[image.PublicId] = count; + } + } + + return Results.Ok(new GetImagesEndpointResponse + { + Images = mapper.MapCollection(sortedImages).ToArray(), + References = references + }); + } + + public sealed record GetImagesEndpointResponse + { + public required ImageDto[] Images { get; init; } + + public required Dictionary? References { get; init; } } } diff --git a/src/Turnierplan.App/Endpoints/Images/SetImageNameEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/SetImageNameEndpoint.cs new file mode 100644 index 00000000..7ed67e2a --- /dev/null +++ b/src/Turnierplan.App/Endpoints/Images/SetImageNameEndpoint.cs @@ -0,0 +1,64 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Extensions; +using Turnierplan.App.Security; +using Turnierplan.Core.PublicId; +using Turnierplan.Dal.Repositories; + +namespace Turnierplan.App.Endpoints.Images; + +internal sealed class SetImageNameEndpoint : EndpointBase +{ + protected override HttpMethod Method => HttpMethod.Patch; + + protected override string Route => "/api/images/{id}/name"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromRoute] PublicId id, + [FromBody] SetImageNameEndpointRequest request, + IImageRepository repository, + IAccessValidator accessValidator, + CancellationToken cancellationToken) + { + if (!Validator.Instance.ValidateAndGetResult(request, out var result)) + { + return result; + } + + var image = await repository.GetByPublicIdAsync(id); + + if (image is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(image, Actions.GenericWrite)) + { + return Results.Forbid(); + } + + image.Name = request.Name.Trim(); + + await repository.UnitOfWork.SaveChangesAsync(cancellationToken); + + return Results.NoContent(); + } + + public sealed record SetImageNameEndpointRequest + { + public required string Name { get; init; } + } + + private sealed class Validator : AbstractValidator + { + public static readonly Validator Instance = new(); + + private Validator() + { + RuleFor(x => x.Name) + .NotEmpty(); + } + } +} diff --git a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs index e03f37df..c511e831 100644 --- a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs @@ -1,9 +1,11 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using SkiaSharp; using Turnierplan.App.Extensions; using Turnierplan.App.Mapping; using Turnierplan.App.Models; +using Turnierplan.App.Options; using Turnierplan.App.Security; using Turnierplan.Core.Image; using Turnierplan.Core.PublicId; @@ -14,9 +16,6 @@ namespace Turnierplan.App.Endpoints.Images; internal sealed class UploadImageEndpoint : EndpointBase { - private const int MinimumImageSizeInPixels = 50; - private const int MaximumImageSizeInPixels = 3000; - protected override HttpMethod Method => HttpMethod.Post; protected override string Route => "/api/images"; @@ -24,16 +23,32 @@ internal sealed class UploadImageEndpoint : EndpointBase protected override Delegate Handler => Handle; private static async Task Handle( - // Note the usage of [FromForm] instead of [FromBody] - [FromForm] UploadImageEndpointRequest request, + [FromForm] UploadImageEndpointRequest request, // Note that [FromForm] is used instead of [FromBody] IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IImageStorage imageStorage, IImageRepository imageRepository, + IOptions turnierplanOptions, + ILogger logger, IMapper mapper, CancellationToken cancellationToken) { - if (!Validator.Instance.ValidateAndGetResult(request, out var result)) + var maxImageSize = turnierplanOptions.Value.ImageMaxSize; + var imageQuality = turnierplanOptions.Value.ImageQuality; + + if (maxImageSize is null or <= 0) + { + logger.LogError($"The '{nameof(TurnierplanOptions.ImageMaxSize)}' value in '{nameof(TurnierplanOptions)}' must be specified and greater than zero."); + return Results.InternalServerError(); + } + + if (imageQuality is null or <= 0 or > 100) + { + logger.LogError($"The '{nameof(TurnierplanOptions.ImageQuality)}' value in '{nameof(TurnierplanOptions)}' must be specified, greater than zero, and less than or equal to 100."); + return Results.InternalServerError(); + } + + if (!new Validator(maxImageSize.Value).ValidateAndGetResult(request, out var result)) { return result; } @@ -67,31 +82,12 @@ private static async Task Handle( return Results.BadRequest("Could not process image."); } - if (imageData.Width < MinimumImageSizeInPixels || imageData.Height < MinimumImageSizeInPixels) - { - return Results.BadRequest($"Image is too small (must be at least {MinimumImageSizeInPixels}px along both sides)."); - } - - if (imageData.Width > MaximumImageSizeInPixels || imageData.Height > MaximumImageSizeInPixels) - { - return Results.BadRequest($"Image is too large (maximum is {MaximumImageSizeInPixels}px along each side)."); - } - - var constraints = ImageConstraints.GetImageConstraints(request.ImageType); - - if (!constraints.IsSizeValid((ushort)imageData.Width, (ushort)imageData.Height)) - { - return Results.BadRequest($"Image dimensions do not meet the constraints: {constraints}"); - } - - imageData = ScaleBitmapToMaximumDimensions(imageData, request.ImageType); - var memoryStream = new MemoryStream(); - var encodedData = imageData.Encode(SKEncodedImageFormat.Webp, 80); // IDEA: Make the quality configurable via app settings + var encodedData = imageData.Encode(SKEncodedImageFormat.Webp, imageQuality.Value); encodedData.SaveTo(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); - var image = new Image(organization, request.ImageName, request.ImageType, "webp", memoryStream.Length, (ushort)imageData.Width, (ushort)imageData.Height); + var image = new Image(organization, request.ImageName.Trim(), "webp", memoryStream.Length, (ushort)imageData.Width, (ushort)imageData.Height); // Dispose here because Image() ctor accesses width and height of imageData imageData.Dispose(); @@ -111,29 +107,6 @@ private static async Task Handle( return Results.Ok(mapper.Map(image)); } - private static SKBitmap ScaleBitmapToMaximumDimensions(SKBitmap imageData, ImageType imageType) - { - var maxWidth = imageType switch - { - ImageType.Logo => 400, - ImageType.Banner => 1600, - _ => throw new ArgumentOutOfRangeException(nameof(imageType), imageType, null) - }; - - if (imageData.Width < maxWidth) - { - return imageData; - } - - var scaleFactor = (float)maxWidth / imageData.Width; - var destinationSize = new SKImageInfo(maxWidth, (int)(imageData.Height * scaleFactor)); - var scaledImage = imageData.Resize(destinationSize, new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)); - - imageData.Dispose(); - - return scaledImage; - } - public sealed record UploadImageEndpointRequest { /// @@ -142,8 +115,6 @@ public sealed record UploadImageEndpointRequest /// public required string OrganizationId { get; init; } - public required ImageType ImageType { get; init; } - public required IFormFile Image { get; init; } public required string ImageName { get; init; } @@ -151,16 +122,11 @@ public sealed record UploadImageEndpointRequest private sealed class Validator : AbstractValidator { - public static readonly Validator Instance = new(); - - private Validator() + public Validator(int maxSize) { - RuleFor(x => x.ImageType) - .IsInEnum(); - RuleFor(x => x.Image.Length) - .LessThanOrEqualTo(8 * 1024 * 1024) - .WithMessage("Image file size must be 8MB or less."); + .LessThanOrEqualTo(maxSize) + .WithMessage($"The maximum allowed image size is {maxSize} bytes."); RuleFor(x => x.ImageName) .NotEmpty(); diff --git a/src/Turnierplan.App/OpenApi/EnumSchemaTransformer.cs b/src/Turnierplan.App/OpenApi/EnumSchemaTransformer.cs index 6c7fdbcf..52d31729 100644 --- a/src/Turnierplan.App/OpenApi/EnumSchemaTransformer.cs +++ b/src/Turnierplan.App/OpenApi/EnumSchemaTransformer.cs @@ -14,7 +14,7 @@ namespace Turnierplan.App.OpenApi; /// to string: /// /// -/// export enum ImageType { +/// export enum MatchType { /// A = 'A', /// B = 'B', /// C = 'C' diff --git a/src/Turnierplan.App/Options/TurnierplanOptions.cs b/src/Turnierplan.App/Options/TurnierplanOptions.cs index aac4dcb2..6ad9ecff 100644 --- a/src/Turnierplan.App/Options/TurnierplanOptions.cs +++ b/src/Turnierplan.App/Options/TurnierplanOptions.cs @@ -12,6 +12,10 @@ internal sealed record TurnierplanOptions public string? PrivacyUrl { get; init; } + public int? ImageMaxSize { get; init; } + + public int? ImageQuality { get; init; } + public string? InitialUserName { get; init; } public string? InitialUserPassword { get; init; } diff --git a/src/Turnierplan.App/Security/AccessValidator.cs b/src/Turnierplan.App/Security/AccessValidator.cs index 85338670..e3077d84 100644 --- a/src/Turnierplan.App/Security/AccessValidator.cs +++ b/src/Turnierplan.App/Security/AccessValidator.cs @@ -82,15 +82,6 @@ public void AddRolesToResponseHeader(IEntityWithRoleAssignments target) var rolesHeaderValue = $"{targetPublicId}={rolesList}"; _httpContext.Response.Headers.Append(RolesHeaderName, rolesHeaderValue); - - if (target is IEntityWithOrganization entityWithOrganization) - { - // TODO: Evaluate if this is still necessary after #309 / #324 - // Always add the organization-level roles, too. This is necessary because for example, the tournament - // page allows the user to upload images. But for uploading images, the authorization check is done - // against the organization. - AddRolesToResponseHeader(entityWithOrganization.Organization); - } } internal static bool IsActionAllowed(IEntityWithRoleAssignments target, Actions.Action action, Principal principal) diff --git a/src/Turnierplan.App/appsettings.json b/src/Turnierplan.App/appsettings.json index 868a4bad..78852a72 100644 --- a/src/Turnierplan.App/appsettings.json +++ b/src/Turnierplan.App/appsettings.json @@ -27,6 +27,8 @@ "Turnierplan": { "ApplicationUrl": "", "ImprintUrl": "", - "PrivacyUrl": "" + "PrivacyUrl": "", + "ImageMaxSize": 8388608, + "ImageQuality": 80 } } diff --git a/src/Turnierplan.Core/Image/Image.cs b/src/Turnierplan.Core/Image/Image.cs index ec8a173b..2dd0149d 100644 --- a/src/Turnierplan.Core/Image/Image.cs +++ b/src/Turnierplan.Core/Image/Image.cs @@ -1,5 +1,4 @@ using Turnierplan.Core.Entity; -using Turnierplan.Core.Exceptions; using Turnierplan.Core.RoleAssignment; namespace Turnierplan.Core.Image; @@ -8,10 +7,8 @@ public sealed class Image : Entity, IEntityWithRoleAssignments, IEn { internal readonly List> _roleAssignments = []; - public Image(Organization.Organization organization, string name, ImageType type, string fileType, long fileSize, ushort width, ushort height) + public Image(Organization.Organization organization, string name, string fileExtension, long fileSize, ushort width, ushort height) { - ValidateImageSize(type, width, height); - organization._images.Add(this); Id = 0; @@ -20,22 +17,20 @@ public Image(Organization.Organization organization, string name, ImageType type Organization = organization; CreatedAt = DateTime.UtcNow; Name = name; - Type = type; - FileType = fileType; + FileExtension = fileExtension; FileSize = fileSize; Width = width; Height = height; } - internal Image(long id, Guid resourceIdentifier, PublicId.PublicId publicId, DateTime createdAt, string name, ImageType type, string fileType, long fileSize, ushort width, ushort height) + internal Image(long id, Guid resourceIdentifier, PublicId.PublicId publicId, DateTime createdAt, string name, string fileExtension, long fileSize, ushort width, ushort height) { Id = id; ResourceIdentifier = resourceIdentifier; PublicId = publicId; CreatedAt = createdAt; Name = name; - Type = type; - FileType = fileType; + FileExtension = fileExtension; FileSize = fileSize; Width = width; Height = height; @@ -55,9 +50,7 @@ internal Image(long id, Guid resourceIdentifier, PublicId.PublicId publicId, Dat public string Name { get; set; } - public ImageType Type { get; } - - public string FileType { get; } + public string FileExtension { get; } public long FileSize { get; } @@ -77,14 +70,4 @@ public void RemoveRoleAssignment(RoleAssignment roleAssignment) { _roleAssignments.Remove(roleAssignment); } - - private static void ValidateImageSize(ImageType type, ushort width, ushort height) - { - var constraints = ImageConstraints.GetImageConstraints(type); - - if (!constraints.IsSizeValid(width, height)) - { - throw new TurnierplanException($"Image with size {width}x{height} does not meet criteria of type {type}: {constraints}"); - } - } } diff --git a/src/Turnierplan.Core/Image/ImageConstraints.cs b/src/Turnierplan.Core/Image/ImageConstraints.cs deleted file mode 100644 index bdbfcbd3..00000000 --- a/src/Turnierplan.Core/Image/ImageConstraints.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Turnierplan.Core.Image; - -public sealed class ImageConstraints -{ - private static readonly ImageConstraints __logoConstraints = new(true); - private static readonly ImageConstraints __bannerConstraints = new(false, 3, 5); - - private readonly bool _mustBeSquare; - private readonly float? _minimumAspectRatio; - private readonly float? _maximumAspectRatio; - - private ImageConstraints(bool mustBeSquare, float? minimumAspectRatio = null, float? maximumAspectRatio = null) - { - if (mustBeSquare) - { - if (minimumAspectRatio is not null || maximumAspectRatio is not null) - { - throw new ArgumentException("Aspect ratio cannot be set if mustBeSquare is true."); - } - - _mustBeSquare = true; - } - else - { - _mustBeSquare = false; - _minimumAspectRatio = minimumAspectRatio ?? throw new ArgumentNullException(nameof(minimumAspectRatio)); - _maximumAspectRatio = maximumAspectRatio ?? throw new ArgumentNullException(nameof(maximumAspectRatio)); - } - } - - public bool IsSizeValid(ushort width, ushort height) - { - if (width == 0 || height == 0) - { - return false; - } - - if (_mustBeSquare) - { - return width == height; - } - - var aspectRatio = (float)width / height; - - return aspectRatio >= _minimumAspectRatio && aspectRatio <= _maximumAspectRatio; - } - - public override string ToString() - { - return _mustBeSquare - ? "Must be square" - : $"Must have aspect ratio between {_minimumAspectRatio:F2} and {_maximumAspectRatio:F2}"; - } - - public static ImageConstraints GetImageConstraints(ImageType imageType) - { - return imageType switch - { - ImageType.Logo => __logoConstraints, - ImageType.Banner => __bannerConstraints, - _ => throw new ArgumentOutOfRangeException(nameof(imageType), imageType, "Invalid image type specified.") - }; - } -} diff --git a/src/Turnierplan.Core/Image/ImageType.cs b/src/Turnierplan.Core/Image/ImageType.cs deleted file mode 100644 index b91cf30b..00000000 --- a/src/Turnierplan.Core/Image/ImageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Turnierplan.Core.Image; - -public enum ImageType -{ - // Note: Don't change enum values (DB serialization) - - Logo = 1, - Banner = 2 -} diff --git a/src/Turnierplan.Core/PlanningRealm/InvitationLink.cs b/src/Turnierplan.Core/PlanningRealm/InvitationLink.cs index 126df461..d31eb4e8 100644 --- a/src/Turnierplan.Core/PlanningRealm/InvitationLink.cs +++ b/src/Turnierplan.Core/PlanningRealm/InvitationLink.cs @@ -1,7 +1,6 @@ using System.Text.Json.Serialization; using Turnierplan.Core.Entity; using Turnierplan.Core.Exceptions; -using Turnierplan.Core.Image; namespace Turnierplan.Core.PlanningRealm; @@ -85,12 +84,12 @@ public void RemoveEntry(InvitationLinkEntry entry) public void SetPrimaryLogo(Image.Image? primaryLogo) { - CheckImageTypeAndSetImage(primaryLogo, () => PrimaryLogo = primaryLogo); + ValidateAndSetImage(primaryLogo, () => PrimaryLogo = primaryLogo); } public void SetSecondaryLogo(Image.Image? secondaryLogo) { - CheckImageTypeAndSetImage(secondaryLogo, () => SecondaryLogo = secondaryLogo); + ValidateAndSetImage(secondaryLogo, () => SecondaryLogo = secondaryLogo); } public bool IsValidUntilSurpassed() @@ -98,7 +97,7 @@ public bool IsValidUntilSurpassed() return ValidUntil.HasValue && ValidUntil.Value < DateTime.UtcNow; } - private void CheckImageTypeAndSetImage(Image.Image? provided, Action apply) + private void ValidateAndSetImage(Image.Image? provided, Action apply) { if (provided is null) { @@ -111,11 +110,6 @@ private void CheckImageTypeAndSetImage(Image.Image? provided, Action apply) throw new TurnierplanException("Cannot assign an image from another organization."); } - if (provided.Type != ImageType.Logo) - { - throw new TurnierplanException($"Cannot assign image because the image's type is not the expected type '{ImageType.Logo}'."); - } - apply(); } diff --git a/src/Turnierplan.Core/Tournament/Tournament.cs b/src/Turnierplan.Core/Tournament/Tournament.cs index e1e140a1..6b8cb1fa 100644 --- a/src/Turnierplan.Core/Tournament/Tournament.cs +++ b/src/Turnierplan.Core/Tournament/Tournament.cs @@ -1,7 +1,6 @@ using Turnierplan.Core.Entity; using Turnierplan.Core.Exceptions; using Turnierplan.Core.Extensions; -using Turnierplan.Core.Image; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament.Comparers; using Turnierplan.Core.Tournament.Definitions; @@ -304,17 +303,17 @@ public void SetVenue(Venue.Venue? venue) public void SetPrimaryLogo(Image.Image? primaryLogo) { - CheckImageTypeAndSetImage(primaryLogo, ImageType.Logo, () => PrimaryLogo = primaryLogo); + ValidateAndSetImage(primaryLogo, () => PrimaryLogo = primaryLogo); } public void SetSecondaryLogo(Image.Image? secondaryLogo) { - CheckImageTypeAndSetImage(secondaryLogo, ImageType.Logo, () => SecondaryLogo = secondaryLogo); + ValidateAndSetImage(secondaryLogo, () => SecondaryLogo = secondaryLogo); } public void SetBannerImage(Image.Image? bannerImage) { - CheckImageTypeAndSetImage(bannerImage, ImageType.Banner, () => BannerImage = bannerImage); + ValidateAndSetImage(bannerImage, () => BannerImage = bannerImage); } public void ShiftToTimezone(TimeZoneInfo timeZone) @@ -1075,7 +1074,7 @@ private int[] GetGroupIdsForConvertingAbstractTeamSelector() .ToArray(); } - private void CheckImageTypeAndSetImage(Image.Image? provided, ImageType expectedType, Action apply) + private void ValidateAndSetImage(Image.Image? provided, Action apply) { if (provided is null) { @@ -1088,11 +1087,6 @@ private void CheckImageTypeAndSetImage(Image.Image? provided, ImageType expected throw new TurnierplanException("Cannot assign an image from another organization."); } - if (provided.Type != expectedType) - { - throw new TurnierplanException($"Cannot assign image because the image's type is not the expected type '{expectedType}'."); - } - apply(); } diff --git a/src/Turnierplan.Dal/EntityConfigurations/ImageEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ImageEntityTypeConfiguration.cs index a2027120..67b50ff1 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ImageEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ImageEntityTypeConfiguration.cs @@ -40,10 +40,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Name) .IsRequired(); - builder.Property(x => x.Type) - .IsRequired(); - - builder.Property(x => x.FileType) + builder.Property(x => x.FileExtension) .IsRequired(); builder.Property(x => x.FileSize) diff --git a/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.Designer.cs b/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.Designer.cs new file mode 100644 index 00000000..737fee25 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.Designer.cs @@ -0,0 +1,1881 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Turnierplan.Dal; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + [DbContext(typeof(TurnierplanContext))] + [Migration("20260201142441_Remove_ImageType")] + partial class Remove_ImageType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("LabelsId") + .HasColumnType("bigint"); + + b.HasKey("ApplicationTeamId", "LabelsId"); + + b.HasIndex("LabelsId"); + + b.ToTable("ApplicationTeamLabel", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecretHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("ApiKeys", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ApiKeyRequests", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Configuration") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GenerationCount") + .HasColumnType("integer"); + + b.Property("LastGeneration") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("TournamentId"); + + b.ToTable("Documents", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Folders", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("ResourceIdentifier") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("ResourceIdentifier") + .IsUnique(); + + b.ToTable("Images", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Organizations", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Contact") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormSession") + .HasColumnType("uuid"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("SourceLinkId") + .HasColumnType("bigint"); + + b.Property("Tag") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FormSession") + .IsUnique(); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("SourceLinkId"); + + b.ToTable("Applications", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChangeLogs", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("ClassId"); + + b.ToTable("ApplicationTeams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactPerson") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.ToTable("InvitationLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLinkEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowNewRegistrations") + .HasColumnType("boolean"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("InvitationLinkId") + .HasColumnType("bigint"); + + b.Property("MaxTeamsPerRegistration") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("InvitationLinkId"); + + b.ToTable("InvitationLinkEntries", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Label", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("Labels", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("PlanningRealms", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TeamLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("TeamTournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationTeamId") + .IsUnique(); + + b.HasIndex("TeamTournamentId", "TeamId") + .IsUnique(); + + b.ToTable("TeamLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TournamentClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("TournamentClasses", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.ToTable("IAM_ApiKey", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.ToTable("IAM_Folder", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("IAM_Image", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("IAM_Organization", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("IAM_PlanningRealm", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TournamentId"); + + b.ToTable("IAM_Tournament", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("VenueId"); + + b.ToTable("IAM_Venue", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AlphabeticalId") + .HasColumnType("character(1)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Groups", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "GroupId", "TeamId"); + + b.HasIndex("TournamentId", "TeamId"); + + b.ToTable("GroupParticipants", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Court") + .HasColumnType("smallint"); + + b.Property("FinalsRound") + .HasColumnType("integer"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("IsCurrentlyPlaying") + .HasColumnType("boolean"); + + b.Property("Kickoff") + .HasColumnType("timestamp with time zone"); + + b.Property("OutcomeType") + .HasColumnType("integer"); + + b.Property("PlayoffPosition") + .HasColumnType("integer"); + + b.Property("ScoreA") + .HasColumnType("integer"); + + b.Property("ScoreB") + .HasColumnType("integer"); + + b.Property("TeamSelectorA") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamSelectorB") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("TournamentId", "GroupId"); + + b.ToTable("Matches", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AssignTeamId") + .HasColumnType("integer"); + + b.Property("AssignTeamTournamentId") + .HasColumnType("bigint"); + + b.Property("HideRanking") + .HasColumnType("boolean"); + + b.Property("PlacementRank") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("AssignTeamTournamentId", "AssignTeamId"); + + b.ToTable("RankingOverwrites", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("EntryFeePaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutOfCompetition") + .HasColumnType("boolean"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Teams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannerImageId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("PublicPageViews") + .HasColumnType("integer"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BannerImageId"); + + b.HasIndex("FolderId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.HasIndex("VenueId"); + + b.ToTable("Tournaments", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsAdministrator") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChange") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEMail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEMail") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique(); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.ToTable("Users", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection>("AddressDetails") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection>("ExternalLinks") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Venues", "turnierplan"); + }); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.ApplicationTeam", null) + .WithMany() + .HasForeignKey("ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.Label", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "ApiKey") + .WithMany("Requests") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Documents") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Folders") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Images") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "PlanningRealm") + .WithMany("Applications") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.InvitationLink", "SourceLink") + .WithMany() + .HasForeignKey("SourceLinkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PlanningRealm"); + + b.Navigation("SourceLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationChangeLog", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.Application", "Application") + .WithMany("ChangeLog") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Turnierplan.Core.PlanningRealm.ApplicationChangeLog+Property", "Properties", b1 => + { + b1.Property("ApplicationChangeLogId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Type") + .HasJsonPropertyName("t"); + + b1.Property("Value") + .IsRequired() + .HasJsonPropertyName("v"); + + b1.HasKey("ApplicationChangeLogId", "__synthesizedOrdinal"); + + b1.ToTable("ApplicationChangeLogs", "turnierplan"); + + b1.ToJson("Properties"); + + b1.WithOwner() + .HasForeignKey("ApplicationChangeLogId"); + }); + + b.Navigation("Application"); + + b.Navigation("Properties"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.Application", "Application") + .WithMany("Teams") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "PlanningRealm") + .WithMany("InvitationLinks") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("Turnierplan.Core.PlanningRealm.InvitationLink+ExternalLink", "ExternalLinks", b1 => + { + b1.Property("InvitationLinkId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Name") + .IsRequired() + .HasJsonPropertyName("n"); + + b1.Property("Url") + .IsRequired() + .HasJsonPropertyName("u"); + + b1.HasKey("InvitationLinkId", "__synthesizedOrdinal"); + + b1.ToTable("InvitationLinks", "turnierplan"); + + b1.ToJson("ExternalLinks"); + + b1.WithOwner() + .HasForeignKey("InvitationLinkId"); + }); + + b.Navigation("ExternalLinks"); + + b.Navigation("PlanningRealm"); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLinkEntry", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.InvitationLink", null) + .WithMany("Entries") + .HasForeignKey("InvitationLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Label", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", null) + .WithMany("Labels") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("PlanningRealms") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TeamLink", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.ApplicationTeam", "ApplicationTeam") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.PlanningRealm.TeamLink", "ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.PlanningRealm.TeamLink", "TeamTournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationTeam"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TournamentClass", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", null) + .WithMany("TournamentClasses") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Folder.Folder", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Venue.Venue", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Groups") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany("Participants") + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithMany() + .HasForeignKey("TournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Matches") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany() + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("RankingOverwrites") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "AssignTeam") + .WithMany() + .HasForeignKey("AssignTeamTournamentId", "AssignTeamId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignTeam"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Teams") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "BannerImage") + .WithMany() + .HasForeignKey("BannerImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Folder.Folder", "Folder") + .WithMany("Tournaments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Tournaments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Venue.Venue", "Venue") + .WithMany("Tournaments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("Turnierplan.Core.Tournament.ComputationConfiguration", "ComputationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.PrimitiveCollection("ComparisonModes") + .IsRequired() + .HasJsonPropertyName("cmp"); + + b1.Property("HigherScoreLoses") + .HasJsonPropertyName("r"); + + b1.Property("MatchDrawnPoints") + .HasJsonPropertyName("d"); + + b1.Property("MatchLostPoints") + .HasJsonPropertyName("l"); + + b1.Property("MatchWonPoints") + .HasJsonPropertyName("w"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("ComputationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.MatchPlanConfiguration", "MatchPlanConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("MatchPlanConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.FinalsRoundConfig", "FinalsRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("EnableThirdPlacePlayoff") + .HasJsonPropertyName("3rd"); + + b2.Property("FirstFinalsRoundOrder") + .HasJsonPropertyName("fo"); + + b2.PrimitiveCollection("TeamSelectors") + .HasJsonPropertyName("ts"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("fr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + + b2.OwnsMany("Turnierplan.Core.Tournament.AdditionalPlayoffConfig", "AdditionalPlayoffs", b3 => + { + b3.Property("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b3.Property("PlayoffPosition") + .HasJsonPropertyName("p"); + + b3.Property("TeamSelectorA") + .IsRequired() + .HasJsonPropertyName("a"); + + b3.Property("TeamSelectorB") + .IsRequired() + .HasJsonPropertyName("b"); + + b3.HasKey("FinalsRoundConfigMatchPlanConfigurationTournamentId", "__synthesizedOrdinal"); + + b3.ToTable("Tournaments", "turnierplan"); + + b3.HasJsonPropertyName("ap"); + + b3.WithOwner() + .HasForeignKey("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + }); + + b2.Navigation("AdditionalPlayoffs"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.GroupRoundConfig", "GroupRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("GroupMatchOrder") + .HasJsonPropertyName("o"); + + b2.Property("GroupPhaseRounds") + .HasJsonPropertyName("r"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("gr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.ScheduleConfig", "ScheduleConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("FinalsPhaseNumberOfCourts") + .HasJsonPropertyName("fc"); + + b2.Property("FinalsPhasePauseTime") + .HasJsonPropertyName("fp"); + + b2.Property("FinalsPhasePlayTime") + .HasJsonPropertyName("fd"); + + b2.Property("FirstMatchKickoff") + .HasJsonPropertyName("f"); + + b2.Property("GroupPhaseNumberOfCourts") + .HasJsonPropertyName("gc"); + + b2.Property("GroupPhasePauseTime") + .HasJsonPropertyName("gp"); + + b2.Property("GroupPhasePlayTime") + .HasJsonPropertyName("gd"); + + b2.Property("PauseBetweenGroupAndFinalsPhase") + .HasJsonPropertyName("p"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("sc"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.Navigation("FinalsRoundConfig"); + + b1.Navigation("GroupRoundConfig"); + + b1.Navigation("ScheduleConfig"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration", "PresentationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.Property("ShowPrimaryLogo") + .HasJsonPropertyName("ol"); + + b1.Property("ShowResults") + .HasJsonPropertyName("o"); + + b1.Property("ShowSecondaryLogo") + .HasJsonPropertyName("sl"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("PresentationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header1", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h1"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header2", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h2"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.Navigation("Header1") + .IsRequired(); + + b1.Navigation("Header2") + .IsRequired(); + }); + + b.Navigation("BannerImage"); + + b.Navigation("ComputationConfiguration") + .IsRequired(); + + b.Navigation("Folder"); + + b.Navigation("MatchPlanConfiguration"); + + b.Navigation("Organization"); + + b.Navigation("PresentationConfiguration") + .IsRequired(); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + + b.Navigation("Venue"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Venues") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Navigation("Requests"); + + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Folders"); + + b.Navigation("Images"); + + b.Navigation("PlanningRealms"); + + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + + b.Navigation("Venues"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.Navigation("ChangeLog"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.Navigation("Applications"); + + b.Navigation("InvitationLinks"); + + b.Navigation("Labels"); + + b.Navigation("RoleAssignments"); + + b.Navigation("TournamentClasses"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Navigation("Documents"); + + b.Navigation("Groups"); + + b.Navigation("Matches"); + + b.Navigation("RankingOverwrites"); + + b.Navigation("RoleAssignments"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.cs b/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.cs new file mode 100644 index 00000000..c45e9629 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260201142441_Remove_ImageType.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + /// + public partial class Remove_ImageType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + schema: "turnierplan", + table: "Images"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Type", + schema: "turnierplan", + table: "Images", + type: "integer", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.Designer.cs b/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.Designer.cs new file mode 100644 index 00000000..7daf64a1 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.Designer.cs @@ -0,0 +1,1881 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Turnierplan.Dal; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + [DbContext(typeof(TurnierplanContext))] + [Migration("20260201142521_Rename_ImageFileExtension")] + partial class Rename_ImageFileExtension + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("LabelsId") + .HasColumnType("bigint"); + + b.HasKey("ApplicationTeamId", "LabelsId"); + + b.HasIndex("LabelsId"); + + b.ToTable("ApplicationTeamLabel", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecretHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("ApiKeys", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ApiKeyRequests", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Configuration") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GenerationCount") + .HasColumnType("integer"); + + b.Property("LastGeneration") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("TournamentId"); + + b.ToTable("Documents", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Folders", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileExtension") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("ResourceIdentifier") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("ResourceIdentifier") + .IsUnique(); + + b.ToTable("Images", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Organizations", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Contact") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormSession") + .HasColumnType("uuid"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("SourceLinkId") + .HasColumnType("bigint"); + + b.Property("Tag") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FormSession") + .IsUnique(); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("SourceLinkId"); + + b.ToTable("Applications", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChangeLogs", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("ClassId"); + + b.ToTable("ApplicationTeams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactPerson") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.ToTable("InvitationLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLinkEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowNewRegistrations") + .HasColumnType("boolean"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("InvitationLinkId") + .HasColumnType("bigint"); + + b.Property("MaxTeamsPerRegistration") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("InvitationLinkId"); + + b.ToTable("InvitationLinkEntries", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Label", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("Labels", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("PlanningRealms", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TeamLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("TeamTournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationTeamId") + .IsUnique(); + + b.HasIndex("TeamTournamentId", "TeamId") + .IsUnique(); + + b.ToTable("TeamLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TournamentClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("TournamentClasses", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.ToTable("IAM_ApiKey", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.ToTable("IAM_Folder", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("IAM_Image", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("IAM_Organization", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("IAM_PlanningRealm", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TournamentId"); + + b.ToTable("IAM_Tournament", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("VenueId"); + + b.ToTable("IAM_Venue", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AlphabeticalId") + .HasColumnType("character(1)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Groups", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "GroupId", "TeamId"); + + b.HasIndex("TournamentId", "TeamId"); + + b.ToTable("GroupParticipants", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Court") + .HasColumnType("smallint"); + + b.Property("FinalsRound") + .HasColumnType("integer"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("IsCurrentlyPlaying") + .HasColumnType("boolean"); + + b.Property("Kickoff") + .HasColumnType("timestamp with time zone"); + + b.Property("OutcomeType") + .HasColumnType("integer"); + + b.Property("PlayoffPosition") + .HasColumnType("integer"); + + b.Property("ScoreA") + .HasColumnType("integer"); + + b.Property("ScoreB") + .HasColumnType("integer"); + + b.Property("TeamSelectorA") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamSelectorB") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("TournamentId", "GroupId"); + + b.ToTable("Matches", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AssignTeamId") + .HasColumnType("integer"); + + b.Property("AssignTeamTournamentId") + .HasColumnType("bigint"); + + b.Property("HideRanking") + .HasColumnType("boolean"); + + b.Property("PlacementRank") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("AssignTeamTournamentId", "AssignTeamId"); + + b.ToTable("RankingOverwrites", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("EntryFeePaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutOfCompetition") + .HasColumnType("boolean"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Teams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannerImageId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("PublicPageViews") + .HasColumnType("integer"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BannerImageId"); + + b.HasIndex("FolderId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.HasIndex("VenueId"); + + b.ToTable("Tournaments", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsAdministrator") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChange") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEMail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEMail") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique(); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.ToTable("Users", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection>("AddressDetails") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection>("ExternalLinks") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Venues", "turnierplan"); + }); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.ApplicationTeam", null) + .WithMany() + .HasForeignKey("ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.Label", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "ApiKey") + .WithMany("Requests") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Documents") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Folders") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Images") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "PlanningRealm") + .WithMany("Applications") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.InvitationLink", "SourceLink") + .WithMany() + .HasForeignKey("SourceLinkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PlanningRealm"); + + b.Navigation("SourceLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationChangeLog", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.Application", "Application") + .WithMany("ChangeLog") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Turnierplan.Core.PlanningRealm.ApplicationChangeLog+Property", "Properties", b1 => + { + b1.Property("ApplicationChangeLogId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Type") + .HasJsonPropertyName("t"); + + b1.Property("Value") + .IsRequired() + .HasJsonPropertyName("v"); + + b1.HasKey("ApplicationChangeLogId", "__synthesizedOrdinal"); + + b1.ToTable("ApplicationChangeLogs", "turnierplan"); + + b1.ToJson("Properties"); + + b1.WithOwner() + .HasForeignKey("ApplicationChangeLogId"); + }); + + b.Navigation("Application"); + + b.Navigation("Properties"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.Application", "Application") + .WithMany("Teams") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "PlanningRealm") + .WithMany("InvitationLinks") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("Turnierplan.Core.PlanningRealm.InvitationLink+ExternalLink", "ExternalLinks", b1 => + { + b1.Property("InvitationLinkId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Name") + .IsRequired() + .HasJsonPropertyName("n"); + + b1.Property("Url") + .IsRequired() + .HasJsonPropertyName("u"); + + b1.HasKey("InvitationLinkId", "__synthesizedOrdinal"); + + b1.ToTable("InvitationLinks", "turnierplan"); + + b1.ToJson("ExternalLinks"); + + b1.WithOwner() + .HasForeignKey("InvitationLinkId"); + }); + + b.Navigation("ExternalLinks"); + + b.Navigation("PlanningRealm"); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLinkEntry", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.PlanningRealm.InvitationLink", null) + .WithMany("Entries") + .HasForeignKey("InvitationLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Label", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", null) + .WithMany("Labels") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("PlanningRealms") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TeamLink", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.ApplicationTeam", "ApplicationTeam") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.PlanningRealm.TeamLink", "ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.PlanningRealm.TeamLink", "TeamTournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationTeam"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.TournamentClass", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", null) + .WithMany("TournamentClasses") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Folder.Folder", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.PlanningRealm.PlanningRealm", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Venue.Venue", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Groups") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany("Participants") + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithMany() + .HasForeignKey("TournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Matches") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany() + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("RankingOverwrites") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "AssignTeam") + .WithMany() + .HasForeignKey("AssignTeamTournamentId", "AssignTeamId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignTeam"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Teams") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "BannerImage") + .WithMany() + .HasForeignKey("BannerImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Folder.Folder", "Folder") + .WithMany("Tournaments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Tournaments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Venue.Venue", "Venue") + .WithMany("Tournaments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("Turnierplan.Core.Tournament.ComputationConfiguration", "ComputationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.PrimitiveCollection("ComparisonModes") + .IsRequired() + .HasJsonPropertyName("cmp"); + + b1.Property("HigherScoreLoses") + .HasJsonPropertyName("r"); + + b1.Property("MatchDrawnPoints") + .HasJsonPropertyName("d"); + + b1.Property("MatchLostPoints") + .HasJsonPropertyName("l"); + + b1.Property("MatchWonPoints") + .HasJsonPropertyName("w"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("ComputationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.MatchPlanConfiguration", "MatchPlanConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("MatchPlanConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.FinalsRoundConfig", "FinalsRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("EnableThirdPlacePlayoff") + .HasJsonPropertyName("3rd"); + + b2.Property("FirstFinalsRoundOrder") + .HasJsonPropertyName("fo"); + + b2.PrimitiveCollection("TeamSelectors") + .HasJsonPropertyName("ts"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("fr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + + b2.OwnsMany("Turnierplan.Core.Tournament.AdditionalPlayoffConfig", "AdditionalPlayoffs", b3 => + { + b3.Property("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b3.Property("PlayoffPosition") + .HasJsonPropertyName("p"); + + b3.Property("TeamSelectorA") + .IsRequired() + .HasJsonPropertyName("a"); + + b3.Property("TeamSelectorB") + .IsRequired() + .HasJsonPropertyName("b"); + + b3.HasKey("FinalsRoundConfigMatchPlanConfigurationTournamentId", "__synthesizedOrdinal"); + + b3.ToTable("Tournaments", "turnierplan"); + + b3.HasJsonPropertyName("ap"); + + b3.WithOwner() + .HasForeignKey("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + }); + + b2.Navigation("AdditionalPlayoffs"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.GroupRoundConfig", "GroupRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("GroupMatchOrder") + .HasJsonPropertyName("o"); + + b2.Property("GroupPhaseRounds") + .HasJsonPropertyName("r"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("gr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.ScheduleConfig", "ScheduleConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("FinalsPhaseNumberOfCourts") + .HasJsonPropertyName("fc"); + + b2.Property("FinalsPhasePauseTime") + .HasJsonPropertyName("fp"); + + b2.Property("FinalsPhasePlayTime") + .HasJsonPropertyName("fd"); + + b2.Property("FirstMatchKickoff") + .HasJsonPropertyName("f"); + + b2.Property("GroupPhaseNumberOfCourts") + .HasJsonPropertyName("gc"); + + b2.Property("GroupPhasePauseTime") + .HasJsonPropertyName("gp"); + + b2.Property("GroupPhasePlayTime") + .HasJsonPropertyName("gd"); + + b2.Property("PauseBetweenGroupAndFinalsPhase") + .HasJsonPropertyName("p"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("sc"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.Navigation("FinalsRoundConfig"); + + b1.Navigation("GroupRoundConfig"); + + b1.Navigation("ScheduleConfig"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration", "PresentationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.Property("ShowPrimaryLogo") + .HasJsonPropertyName("ol"); + + b1.Property("ShowResults") + .HasJsonPropertyName("o"); + + b1.Property("ShowSecondaryLogo") + .HasJsonPropertyName("sl"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("PresentationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header1", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h1"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header2", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h2"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.Navigation("Header1") + .IsRequired(); + + b1.Navigation("Header2") + .IsRequired(); + }); + + b.Navigation("BannerImage"); + + b.Navigation("ComputationConfiguration") + .IsRequired(); + + b.Navigation("Folder"); + + b.Navigation("MatchPlanConfiguration"); + + b.Navigation("Organization"); + + b.Navigation("PresentationConfiguration") + .IsRequired(); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + + b.Navigation("Venue"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Venues") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Navigation("Requests"); + + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Folders"); + + b.Navigation("Images"); + + b.Navigation("PlanningRealms"); + + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + + b.Navigation("Venues"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.Application", b => + { + b.Navigation("ChangeLog"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.ApplicationTeam", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.InvitationLink", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Turnierplan.Core.PlanningRealm.PlanningRealm", b => + { + b.Navigation("Applications"); + + b.Navigation("InvitationLinks"); + + b.Navigation("Labels"); + + b.Navigation("RoleAssignments"); + + b.Navigation("TournamentClasses"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Navigation("Documents"); + + b.Navigation("Groups"); + + b.Navigation("Matches"); + + b.Navigation("RankingOverwrites"); + + b.Navigation("RoleAssignments"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.cs b/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.cs new file mode 100644 index 00000000..3e6c364d --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260201142521_Rename_ImageFileExtension.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + /// + public partial class Rename_ImageFileExtension : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "FileType", + schema: "turnierplan", + table: "Images", + newName: "FileExtension"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "FileExtension", + schema: "turnierplan", + table: "Images", + newName: "FileType"); + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs index 3f3727b6..3a1a6f5b 100644 --- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs +++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs @@ -205,13 +205,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") + b.Property("FileExtension") .IsRequired() .HasColumnType("text"); + b.Property("FileSize") + .HasColumnType("bigint"); + b.Property("Height") .HasColumnType("integer"); @@ -228,9 +228,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResourceIdentifier") .HasColumnType("uuid"); - b.Property("Type") - .HasColumnType("integer"); - b.Property("Width") .HasColumnType("integer"); diff --git a/src/Turnierplan.Dal/Repositories/ImageRepository.cs b/src/Turnierplan.Dal/Repositories/ImageRepository.cs index 0b41a574..1357e440 100644 --- a/src/Turnierplan.Dal/Repositories/ImageRepository.cs +++ b/src/Turnierplan.Dal/Repositories/ImageRepository.cs @@ -4,10 +4,15 @@ namespace Turnierplan.Dal.Repositories; -public interface IImageRepository : IRepositoryWithPublicId; +public interface IImageRepository : IRepositoryWithPublicId +{ + Task CountNumberOfReferencingTournamentsAsync(long imageId); +} internal sealed class ImageRepository(TurnierplanContext context) : RepositoryBaseWithPublicId(context), IImageRepository { + private readonly TurnierplanContext _context = context; + public override Task GetByPublicIdAsync(PublicId id) { return DbSet.Where(x => x.PublicId == id) @@ -16,4 +21,14 @@ internal sealed class ImageRepository(TurnierplanContext context) : RepositoryBa .AsSplitQuery() .FirstOrDefaultAsync(); } + + public async Task CountNumberOfReferencingTournamentsAsync(long imageId) + { + var count = await _context.Tournaments + .Where(x => x.PrimaryLogo!.Id == imageId || x.SecondaryLogo!.Id == imageId || x.BannerImage!.Id == imageId) + .CountAsync(); + + return count; + } + } diff --git a/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs b/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs index b365d523..e9433571 100644 --- a/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs +++ b/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs @@ -46,7 +46,7 @@ internal sealed class OrganizationRepository(TurnierplanContext context) : Repos if (includes.HasFlag(IOrganizationRepository.Includes.Tournaments)) { - query = query.Include(x => x.Tournaments).ThenInclude(x => x.Folder); + query = query.Include(x => x.Tournaments).ThenInclude(x => x.Folder).ThenInclude(x => x!.RoleAssignments); } if (includes.HasFlag(IOrganizationRepository.Includes.Venues)) @@ -61,7 +61,7 @@ internal sealed class OrganizationRepository(TurnierplanContext context) : Repos if (includes.HasFlag(IOrganizationRepository.Includes.Images)) { - query = query.Include(x => x.Images); + query = query.Include(x => x.Images).ThenInclude(x => x.RoleAssignments); } if (includes.HasFlag(IOrganizationRepository.Includes.ApiKeys)) diff --git a/src/Turnierplan.ImageStorage/Azure/AzureImageStorage.cs b/src/Turnierplan.ImageStorage/Azure/AzureImageStorage.cs index 0ab2f9e5..4f7e92fe 100644 --- a/src/Turnierplan.ImageStorage/Azure/AzureImageStorage.cs +++ b/src/Turnierplan.ImageStorage/Azure/AzureImageStorage.cs @@ -141,6 +141,6 @@ public void Dispose() private static string GetBlobName(Image image) { - return $"{image.CreatedAt.Year}/{image.CreatedAt.Month:D2}/{image.ResourceIdentifier}.{image.FileType}"; + return $"{image.CreatedAt.Year}/{image.CreatedAt.Month:D2}/{image.ResourceIdentifier}.{image.FileExtension}"; } } diff --git a/src/Turnierplan.ImageStorage/Local/LocalImageStorage.cs b/src/Turnierplan.ImageStorage/Local/LocalImageStorage.cs index 85be3a5a..3534a99a 100644 --- a/src/Turnierplan.ImageStorage/Local/LocalImageStorage.cs +++ b/src/Turnierplan.ImageStorage/Local/LocalImageStorage.cs @@ -114,6 +114,6 @@ private string GetImageFullPath(Image image) private static string GetImageFileName(Image image) { - return $"{image.ResourceIdentifier}.{image.FileType}"; + return $"{image.ResourceIdentifier}.{image.FileExtension}"; } } diff --git a/src/Turnierplan.ImageStorage/S3/S3ImageStorage.cs b/src/Turnierplan.ImageStorage/S3/S3ImageStorage.cs index f1a3705a..0c54c038 100644 --- a/src/Turnierplan.ImageStorage/S3/S3ImageStorage.cs +++ b/src/Turnierplan.ImageStorage/S3/S3ImageStorage.cs @@ -151,6 +151,6 @@ public void Dispose() private static string GetObjectKey(Image image) { - return $"images/{image.CreatedAt.Year}/{image.CreatedAt.Month:D2}/{image.ResourceIdentifier}.{image.FileType}"; + return $"images/{image.CreatedAt.Year}/{image.CreatedAt.Month:D2}/{image.ResourceIdentifier}.{image.FileExtension}"; } } diff --git a/src/Turnierplan.PdfRendering/Tracing/DocumentRendererActivitySource.cs b/src/Turnierplan.PdfRendering/Tracing/DocumentRendererActivitySource.cs index 01822552..4dff6bf7 100644 --- a/src/Turnierplan.PdfRendering/Tracing/DocumentRendererActivitySource.cs +++ b/src/Turnierplan.PdfRendering/Tracing/DocumentRendererActivitySource.cs @@ -40,7 +40,8 @@ internal static class DocumentRendererActivitySource } activity.SetTag("turnierplan.image.id", image.PublicId.ToString()); - activity.SetTag("turnierplan.image.type", image.Type.ToString()); + activity.SetTag("turnierplan.image.file_size", image.FileSize); + activity.SetTag("turnierplan.image.file_extension", image.FileExtension); activity.SetTag("turnierplan.image_usage", usage); activity.Start();