diff --git a/src/Turnierplan.App.Test.Functional/Routes.cs b/src/Turnierplan.App.Test.Functional/Routes.cs index 9de37e9e..2d00b710 100644 --- a/src/Turnierplan.App.Test.Functional/Routes.cs +++ b/src/Turnierplan.App.Test.Functional/Routes.cs @@ -14,8 +14,16 @@ public static class Identity public static string Login() => "/api/identity/login"; } + public static class Organizations + { + public static string Create() => "/api/organizations"; + } + public static class Users { + public static string List() => "/api/users"; + public static string Create() => "/api/users"; public static string Delete(Guid id) => $"/api/users/{id}"; + public static string Update(Guid id) => $"/api/users/{id}"; } } diff --git a/src/Turnierplan.App.Test.Functional/Scenarios.cs b/src/Turnierplan.App.Test.Functional/Scenarios.cs index a8c1ea09..0d63df35 100644 --- a/src/Turnierplan.App.Test.Functional/Scenarios.cs +++ b/src/Turnierplan.App.Test.Functional/Scenarios.cs @@ -1,5 +1,8 @@ +using System.Net; +using System.Net.Http.Json; using FluentAssertions; using FluentAssertions.Extensions; +using Turnierplan.App.Models; using Turnierplan.Core.ApiKey; using Turnierplan.Core.Extensions; using Turnierplan.Core.Organization; @@ -55,4 +58,43 @@ public async Task When_ApiKey_And_User_Are_Deleted_The_Role_Assignments_Are_Also _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(0); _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(0); } + + [Fact] + public async Task New_User_Can_Not_Create_Organization_Unless_Explicitly_Granted_Permission() + { + const string newUserName = "test_user"; + const string newUserPassword = "test123"; + + var resp = await _testServer.Client.PostAsJsonAsync( + Routes.Users.Create(), + new { UserName = newUserName, Password = newUserPassword }, + TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + var userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword); + resp = await userClient.PostAsJsonAsync( + Routes.Organizations.Create(), + new { Name = "test_org" }, + TestContext.Current.CancellationToken); + resp.StatusCode.Should().Be(HttpStatusCode.Forbidden); + + // extra step required to get ID of new user + resp = await _testServer.Client.GetAsync(Routes.Users.List(), TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + var allUsers = await resp.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); + var newUserId = allUsers!.Single(x => x.UserName.Equals(newUserName)).Id; + + resp = await _testServer.Client.PutAsJsonAsync( + Routes.Users.Update(newUserId), + new { UserName = newUserName, IsAdministrator = false, AllowCreateOrganization = true, UpdatePassword = false }, + TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword); + resp = await userClient.PostAsJsonAsync( + Routes.Organizations.Create(), + new { Name = "test_org" }, + TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + } } diff --git a/src/Turnierplan.App.Test.Functional/TestServer.cs b/src/Turnierplan.App.Test.Functional/TestServer.cs index 92cb0ca0..bf0ec2e8 100644 --- a/src/Turnierplan.App.Test.Functional/TestServer.cs +++ b/src/Turnierplan.App.Test.Functional/TestServer.cs @@ -33,30 +33,35 @@ public TestServer() { var ctx = scope.ServiceProvider.GetRequiredService(); - var user = new User(username) - { - IsAdministrator = true - }; + var user = new User(username); + user.SetIsAdministrator(true); user.UpdatePassword(scope.ServiceProvider.GetRequiredService>().HashPassword(user, password)); ctx.Users.Add(user); ctx.SaveChanges(); } + Client = CreateNewClientAndLogIn(username, password); + } + + public HttpClient Client { get; } + + public HttpClient CreateNewClientAndLogIn(string username, string password) + { var loginRequest = new HttpRequestMessage(HttpMethod.Post, Routes.Identity.Login()) { Content = JsonContent.Create(new { UserName = username, Password = password}) }; - Client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); - var loginResponseTask = Client.SendAsync(loginRequest); + var client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); + var loginResponseTask = client.SendAsync(loginRequest); loginResponseTask.Wait(); var loginResponse = loginResponseTask.Result; loginResponse.EnsureSuccessStatusCode(); - } - public HttpClient Client { get; } + return client; + } public void ExecuteContextAction(Action action) { diff --git a/src/Turnierplan.App/Client/src/app/core/services/authentication.service.ts b/src/Turnierplan.App/Client/src/app/core/services/authentication.service.ts index 770f107f..17befb15 100644 --- a/src/Turnierplan.App/Client/src/app/core/services/authentication.service.ts +++ b/src/Turnierplan.App/Client/src/app/core/services/authentication.service.ts @@ -17,6 +17,7 @@ interface TurnierplanAccessToken { userName: string; fullName: string; adm?: string; + createorg?: string; uid: string; } @@ -32,6 +33,7 @@ export class AuthenticationService implements OnDestroy { private static readonly localStorageUserFullNameKey = 'tp_id_userFullName'; private static readonly localStorageUserEMailKey = 'tp_id_userEMail'; private static readonly localStorageUserAdministratorKey = 'tp_id_userAdmin'; + private static readonly localStorageUserAllowCreateOrganizationKey = 'tp_id_userAllowCreateOrg'; private static readonly localStorageAccessTokenExpiryKey = 'tp_id_accTokenExp'; private static readonly localStorageRefreshTokenExpiryKey = 'tp_id_rfsTokenExp'; @@ -41,6 +43,7 @@ export class AuthenticationService implements OnDestroy { AuthenticationService.localStorageUserFullNameKey, AuthenticationService.localStorageUserEMailKey, AuthenticationService.localStorageUserAdministratorKey, + AuthenticationService.localStorageUserAllowCreateOrganizationKey, AuthenticationService.localStorageAccessTokenExpiryKey, AuthenticationService.localStorageRefreshTokenExpiryKey ]; @@ -93,6 +96,7 @@ export class AuthenticationService implements OnDestroy { decodedAccessToken.fullName, decodedAccessToken.mail, decodedAccessToken.adm === 'true', + decodedAccessToken.createorg === 'true', decodedAccessToken.exp, decodedRefreshToken.exp ); @@ -146,6 +150,12 @@ export class AuthenticationService implements OnDestroy { return this.authentication$.pipe(map(() => localStorage.getItem(AuthenticationService.localStorageUserAdministratorKey) === 'true')); } + public checkIfUserIsAllowedToCreateOrganization(): Observable { + return this.authentication$.pipe( + map(() => localStorage.getItem(AuthenticationService.localStorageUserAllowCreateOrganizationKey) === 'true') + ); + } + public changePassword( userName: string, newPassword: string, @@ -218,6 +228,7 @@ export class AuthenticationService implements OnDestroy { decodedAccessToken.fullName, decodedAccessToken.mail, decodedAccessToken.adm === 'true', + decodedAccessToken.createorg === 'true', decodedAccessToken.exp, decodedRefreshToken.exp ); @@ -303,6 +314,7 @@ export class AuthenticationService implements OnDestroy { userFullName: string | undefined, userEMail: string | undefined, userIsAdmin: boolean, + userAllowCreateOrg: boolean, accessTokenExpiry: number, refreshTokenExpiry: number ): void { @@ -311,6 +323,7 @@ export class AuthenticationService implements OnDestroy { localStorage.setItem(AuthenticationService.localStorageUserFullNameKey, userFullName ?? ''); localStorage.setItem(AuthenticationService.localStorageUserEMailKey, userEMail ?? ''); localStorage.setItem(AuthenticationService.localStorageUserAdministratorKey, `${userIsAdmin}`); + localStorage.setItem(AuthenticationService.localStorageUserAllowCreateOrganizationKey, `${userAllowCreateOrg}`); localStorage.setItem(AuthenticationService.localStorageAccessTokenExpiryKey, `${accessTokenExpiry}`); localStorage.setItem(AuthenticationService.localStorageRefreshTokenExpiryKey, `${refreshTokenExpiry}`); diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index 5ffc1f30..140b5aab 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -86,8 +86,8 @@ export const de = { Badges: { OrganizationCount: 'Organisationen' }, - NoOrganizations: - 'Sie sind keinen Organisationen zugehörig.\nErstellen Sie eine neue Organisation, um Turniere anzulegen und zu bearbeiten', + NoOrganizations: 'Sie sind keinen Organisationen zugehörig.', + CreateNewOrganization: 'Erstellen Sie eine neue Organisation, um Turniere anzulegen und zu bearbeiten', NewOrganization: 'Neue Organisation', OrganizationTile: { Open: 'öffnen' @@ -109,6 +109,8 @@ export const de = { EMail: 'E-Mail', CreatedAt: 'Erstellt am', LastPasswordChange: 'Letzte Passwortänderung', + AllowCreateOrganization: 'Org. Erstellen', + AllowCreateOrganizationTooltip: 'Legt fest, ob dieser Benutzer neue Organisationen erstellen darf.', Administrator: 'Admin' }, EditUser: { @@ -119,6 +121,7 @@ export const de = { FullName: 'Name:', Email: 'E-Mailadresse', EmailInvalid: 'Die eingegebene E-Mailadresse ist ungültig.', + AllowCreateOrganization: 'Benutzer darf neue Organisationen anlegen', IsAdministrator: 'Administrator', AdministratorWarning: 'Mit Administratorrechten kann dieser Nutzer ALLES machen!', UpdatePassword: 'Passwort ändern', @@ -152,6 +155,8 @@ export const de = { PasswordInvalid: 'Das eingegebene Passwort ist ungültig.' }, UserNotice: 'Der erstellte Nutzer kann sich unmittelbar danach mit Benutzername und Passwort anmelden.', + AllowCreateOrganizationNotice: + 'Neue Benutzer können standardmäßig keine Organisationen anlegen. Dies kann in der "Benutzer bearbeiten"-Ansicht konfiguriert werden.', Submit: 'Erstellen' }, CreateOrganization: { @@ -162,7 +167,7 @@ export const de = { NameInvalid: 'Der Name einer neuen Organisation darf nicht leer sein.', NameValid: 'Dieser Name kann verwendet werden.' }, - UserNotice: 'Eine Organisation ist z.B. Ihr Sportverein oder Ihre Firma.', + OrganizationExamples: 'Eine Organisation ist z.B. Ihr Sportverein oder Ihre Firma.', Submit: 'Erstellen' }, ViewOrganization: { diff --git a/src/Turnierplan.App/Client/src/app/portal/directives/allow-create-organization.directive.ts b/src/Turnierplan.App/Client/src/app/portal/directives/allow-create-organization.directive.ts new file mode 100644 index 00000000..51e151a5 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/directives/allow-create-organization.directive.ts @@ -0,0 +1,34 @@ +import { Directive, ElementRef, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; + +import { AuthenticationService } from '../../core/services/authentication.service'; + +@Directive({ selector: '[tpAllowCreateOrganization]' }) +export class AllowCreateOrganizationDirective implements OnInit, OnDestroy { + private readonly destroyed$ = new Subject(); + + constructor( + private readonly templateRef: TemplateRef, + private readonly viewContainer: ViewContainerRef, + private readonly authenticationService: AuthenticationService + ) {} + + public ngOnInit(): void { + this.authenticationService + .checkIfUserIsAllowedToCreateOrganization() + .pipe(takeUntil(this.destroyed$)) + .subscribe({ + next: (allowCreateOrganization) => { + this.viewContainer.clear(); + if (allowCreateOrganization) { + this.viewContainer.createEmbeddedView(this.templateRef); + } + } + }); + } + + public ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.html index 2bca99bb..65386207 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.html @@ -17,12 +17,18 @@ - - - - - - + + + + + + + @@ -35,13 +41,18 @@ + -
+ + +
{{ user.eMail ?? '' }} {{ user.createdAt | translateDate: 'medium' }} {{ user.lastPasswordChange | translateDate: 'medium' }} + @if (user.allowCreateOrganization) { + + } + @if (user.isAdministrator) { } -
+
+
} + @if (!isAdministratorControl.value) { +
+ + +
+ } +
0 ? formValue.fullName : undefined, eMail: (formValue.eMail ?? '').trim().length > 0 ? formValue.eMail : undefined, isAdministrator: formValue.isAdministrator, + allowCreateOrganization: formValue.isAdministrator || formValue.allowCreateOrganization, updatePassword: formValue.updatePassword, password: formValue.updatePassword ? formValue.password : undefined }; diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/create-organization/create-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/create-organization/create-organization.component.html index 2b222270..536340c1 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/create-organization/create-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/create-organization/create-organization.component.html @@ -20,7 +20,7 @@
- +

diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/create-user/create-user.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/create-user/create-user.component.html index dcd9b078..651d0996 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/create-user/create-user.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/create-user/create-user.component.html @@ -58,6 +58,13 @@
+
+ + +
+ +
+
@if (organizations.length === 0) {
-
+
- + +
} @else { diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/landing-page/landing-page.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/landing-page/landing-page.component.ts index 00485360..54992e00 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/landing-page/landing-page.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/landing-page/landing-page.component.ts @@ -15,6 +15,7 @@ import { UpdatesCheckComponent } from '../../components/updates-check/updates-ch import { OrganizationDto } from '../../../api/models/organization-dto'; import { TurnierplanApi } from '../../../api/turnierplan-api'; import { getOrganizations } from '../../../api/fn/organizations/get-organizations'; +import { AllowCreateOrganizationDirective } from '../../directives/allow-create-organization.directive'; @Component({ templateUrl: './landing-page.component.html', @@ -29,7 +30,8 @@ import { getOrganizations } from '../../../api/fn/organizations/get-organization BadgeComponent, TranslatePipe, E2eDirective, - UpdatesCheckComponent + UpdatesCheckComponent, + AllowCreateOrganizationDirective ] }) export class LandingPageComponent implements OnInit { diff --git a/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs b/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs index 8ce54528..9d468db6 100644 --- a/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs +++ b/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs @@ -53,6 +53,11 @@ protected string CreateTokenForUser(User user, bool isRefreshToken) { claims.Add(new Claim(ClaimTypes.Administrator, "true")); } + + if (user.AllowCreateOrganization) + { + claims.Add(new Claim(ClaimTypes.AllowCreateOrg, "true")); + } } var identityOptions = _options.CurrentValue; diff --git a/src/Turnierplan.App/Endpoints/Organizations/CreateOrganizationEndpoint.cs b/src/Turnierplan.App/Endpoints/Organizations/CreateOrganizationEndpoint.cs index fa689436..346cb2b5 100644 --- a/src/Turnierplan.App/Endpoints/Organizations/CreateOrganizationEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Organizations/CreateOrganizationEndpoint.cs @@ -40,6 +40,11 @@ private static async Task Handle( return Results.Unauthorized(); } + if (!user.AllowCreateOrganization) + { + return Results.Forbid(); + } + var organization = new Organization(request.Name.Trim()); organization.AddRoleAssignment(Role.Owner, user.AsPrincipal()); diff --git a/src/Turnierplan.App/Endpoints/Users/UpdateUserEndpoint.cs b/src/Turnierplan.App/Endpoints/Users/UpdateUserEndpoint.cs index 62c85cb4..beeb132b 100644 --- a/src/Turnierplan.App/Endpoints/Users/UpdateUserEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Users/UpdateUserEndpoint.cs @@ -53,8 +53,9 @@ private static async Task Handle( } user.FullName = request.FullName?.Trim(); - user.IsAdministrator = request.IsAdministrator; + user.SetIsAdministrator(request.IsAdministrator); + user.SetAllowCreateOrganization(request.AllowCreateOrganization); user.SetUserName(request.UserName); user.SetEmailAddress(request.EMail); @@ -76,9 +77,11 @@ public sealed record UpdateUserEndpointRequest public string? EMail { get; init; } - public bool IsAdministrator { get; init; } + public required bool IsAdministrator { get; init; } - public bool UpdatePassword { get; init; } + public required bool AllowCreateOrganization { get; init; } + + public required bool UpdatePassword { get; init; } public string? Password { get; init; } } @@ -101,6 +104,11 @@ private Validator() .EmailAddress() .When(x => x.EMail is not null); + RuleFor(x => x.AllowCreateOrganization) + .Equal(true) + .When(x => x.IsAdministrator) + .WithMessage($"'{nameof(UpdateUserEndpointRequest.AllowCreateOrganization)}' must be true when '{nameof(UpdateUserEndpointRequest.IsAdministrator)}' is true."); + RuleFor(x => x.Password) .Null() .When(x => !x.UpdatePassword); diff --git a/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs b/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs index 6f5c168a..ceaa5d4c 100644 --- a/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs +++ b/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs @@ -109,10 +109,10 @@ private static async Task EnsureInitialUserCreatedAsync(IServiceProvider service var initialUser = new User(initialUserName) { - FullName = "Administrator", - IsAdministrator = true + FullName = "Administrator" }; + initialUser.SetIsAdministrator(true); initialUser.UpdatePassword(passwordHasher.HashPassword(initialUser, initialUserPassword)); await context.Users.AddAsync(initialUser); diff --git a/src/Turnierplan.App/Mapping/Rules/UserMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/UserMappingRule.cs index e5b3dce6..38c56871 100644 --- a/src/Turnierplan.App/Mapping/Rules/UserMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/UserMappingRule.cs @@ -15,7 +15,8 @@ protected override UserDto Map(IMapper mapper, MappingContext context, User sour FullName = source.FullName, EMail = source.EMail, LastPasswordChange = source.LastPasswordChange, - IsAdministrator = source.IsAdministrator + IsAdministrator = source.IsAdministrator, + AllowCreateOrganization = source.AllowCreateOrganization }; } } diff --git a/src/Turnierplan.App/Models/UserDto.cs b/src/Turnierplan.App/Models/UserDto.cs index 08b503f1..5590dedd 100644 --- a/src/Turnierplan.App/Models/UserDto.cs +++ b/src/Turnierplan.App/Models/UserDto.cs @@ -15,4 +15,6 @@ public sealed record UserDto public required DateTime LastPasswordChange { get; init; } public required bool IsAdministrator { get; init; } + + public required bool AllowCreateOrganization { get; init; } } diff --git a/src/Turnierplan.App/Security/ClaimTypes.cs b/src/Turnierplan.App/Security/ClaimTypes.cs index 553b6a12..356b8c51 100644 --- a/src/Turnierplan.App/Security/ClaimTypes.cs +++ b/src/Turnierplan.App/Security/ClaimTypes.cs @@ -3,6 +3,7 @@ namespace Turnierplan.App.Security; internal static class ClaimTypes { public const string Administrator = "adm"; + public const string AllowCreateOrg = "createorg"; public const string EMailAddress = "mail"; public const string FullName = "fullName"; public const string PrincipalKind = "principalkind"; diff --git a/src/Turnierplan.Core.Test.Unit/User/UserTest.cs b/src/Turnierplan.Core.Test.Unit/User/UserTest.cs new file mode 100644 index 00000000..25cdcb04 --- /dev/null +++ b/src/Turnierplan.Core.Test.Unit/User/UserTest.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using Turnierplan.Core.Exceptions; +using Xunit; + +namespace Turnierplan.Core.Test.Unit.User; + +public sealed class UserTest +{ + [Theory] + [InlineData(true, true, false, true)] + [InlineData(false, true, true, true)] + [InlineData(false, false, true, true)] + public void User___When_SetIsAdministrator_Is_Called___AllowCreateOrganization_Is_Updated_As_Expected( + bool currentIsAdministrator, + bool currentAllowCreateOrganization, + bool setIsAdministrator, + bool expectedAllowCreateOrganization) + { + var user = new Core.User.User(Guid.Empty, Guid.Empty, DateTime.UtcNow, string.Empty, string.Empty, null, null, null, string.Empty, currentIsAdministrator, currentAllowCreateOrganization, DateTime.UtcNow, Guid.Empty); + + user.IsAdministrator.Should().Be(currentIsAdministrator); + user.AllowCreateOrganization.Should().Be(currentAllowCreateOrganization); + + user.SetIsAdministrator(setIsAdministrator); + + user.IsAdministrator.Should().Be(setIsAdministrator); + user.AllowCreateOrganization.Should().Be(expectedAllowCreateOrganization); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void User___When_SetAllowCreateOrganization_Is_Called_On_Non_Admin_User___Works_As_Expected( + bool currentAllowCreateOrganization, + bool setAllowCreateOrganization) + { + var user = new Core.User.User(Guid.Empty, Guid.Empty, DateTime.UtcNow, string.Empty, string.Empty, null, null, null, string.Empty, false, currentAllowCreateOrganization, DateTime.UtcNow, Guid.Empty); + + user.IsAdministrator.Should().Be(false); + user.AllowCreateOrganization.Should().Be(currentAllowCreateOrganization); + + user.SetAllowCreateOrganization(setAllowCreateOrganization); + + user.AllowCreateOrganization.Should().Be(setAllowCreateOrganization); + } + + [Fact] + public void User___When_SetAllowCreateOrganization_Is_Called_With_False_On_Admin_User___Works_As_Expected() + { + var user = new Core.User.User(Guid.Empty, Guid.Empty, DateTime.UtcNow, string.Empty, string.Empty, null, null, null, string.Empty, true, true, DateTime.UtcNow, Guid.Empty); + + user.IsAdministrator.Should().Be(true); + user.AllowCreateOrganization.Should().Be(true); + + var action = () => user.SetAllowCreateOrganization(false); + action.Should().ThrowExactly().WithMessage("'AllowCreateOrganization' cannot be set to false for an administrator user."); + } +} diff --git a/src/Turnierplan.Core/User/User.cs b/src/Turnierplan.Core/User/User.cs index d1f2bde7..0e91db82 100644 --- a/src/Turnierplan.Core/User/User.cs +++ b/src/Turnierplan.Core/User/User.cs @@ -1,5 +1,6 @@ using System.Globalization; using Turnierplan.Core.Entity; +using Turnierplan.Core.Exceptions; namespace Turnierplan.Core.User; @@ -7,7 +8,6 @@ public sealed class User : Entity { public User(string userName) { - Id = Guid.NewGuid(); PrincipalId = Guid.NewGuid(); CreatedAt = DateTime.UtcNow; @@ -22,7 +22,7 @@ public User(string userName) SecurityStamp = Guid.Empty; } - internal User(Guid id, Guid principalId, DateTime createdAt, string userName, string normalizedUserName, string? fullName, string? eMail, string? normalizedEMail, string passwordHash, bool isAdministrator, DateTime lastPasswordChange, Guid securityStamp) + internal User(Guid id, Guid principalId, DateTime createdAt, string userName, string normalizedUserName, string? fullName, string? eMail, string? normalizedEMail, string passwordHash, bool isAdministrator, bool allowCreateOrganization, DateTime lastPasswordChange, Guid securityStamp) { Id = id; PrincipalId = principalId; @@ -34,6 +34,7 @@ internal User(Guid id, Guid principalId, DateTime createdAt, string userName, st NormalizedEMail = normalizedEMail; PasswordHash = passwordHash; IsAdministrator = isAdministrator; + AllowCreateOrganization = allowCreateOrganization; LastPasswordChange = lastPasswordChange; SecurityStamp = securityStamp; } @@ -56,7 +57,9 @@ internal User(Guid id, Guid principalId, DateTime createdAt, string userName, st public string PasswordHash { get; private set; } - public bool IsAdministrator { get; set; } + public bool IsAdministrator { get; private set; } + + public bool AllowCreateOrganization { get; private set; } public DateTime LastPasswordChange { get; private set; } @@ -100,5 +103,25 @@ public void SetEmailAddress(string? newEmail) SecurityStamp = Guid.NewGuid(); } + public void SetIsAdministrator(bool isAdministrator) + { + IsAdministrator = isAdministrator; + + if (isAdministrator) + { + AllowCreateOrganization = true; + } + } + + public void SetAllowCreateOrganization(bool allowCreateOrganization) + { + if (IsAdministrator && !allowCreateOrganization) + { + throw new TurnierplanException($"'{nameof(AllowCreateOrganization)}' cannot be set to false for an administrator user."); + } + + AllowCreateOrganization = allowCreateOrganization; + } + public static string Normalize(string value) => value.Trim().ToUpper(CultureInfo.InvariantCulture); } diff --git a/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs index 3e8109d1..cdd17caa 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs @@ -48,9 +48,18 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.PasswordHash) .IsRequired(); + builder.Property(x => x.IsAdministrator) + .IsRequired(); + + builder.Property(x => x.AllowCreateOrganization) + .IsRequired(); + builder.Property(x => x.LastPasswordChange) .IsRequired(); + builder.Property(x => x.LastLogin) + .IsRequired(false); + builder.Property(x => x.SecurityStamp) .IsRequired(); } diff --git a/src/Turnierplan.Dal/Migrations/20260307185525_Add_UserAllowCreateOrganization.Designer.cs b/src/Turnierplan.Dal/Migrations/20260307185525_Add_UserAllowCreateOrganization.Designer.cs new file mode 100644 index 00000000..17057b72 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260307185525_Add_UserAllowCreateOrganization.Designer.cs @@ -0,0 +1,1884 @@ +// +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("20260307185525_Add_UserAllowCreateOrganization")] + partial class Add_UserAllowCreateOrganization + { + /// + 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("AllowCreateOrganization") + .HasColumnType("boolean"); + + 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/20260307185525_Add_UserAllowCreateOrganization.cs b/src/Turnierplan.Dal/Migrations/20260307185525_Add_UserAllowCreateOrganization.cs new file mode 100644 index 00000000..ec341785 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260307185525_Add_UserAllowCreateOrganization.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + /// + public partial class Add_UserAllowCreateOrganization : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AllowCreateOrganization", + schema: "turnierplan", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AllowCreateOrganization", + schema: "turnierplan", + table: "Users"); + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs index 3a1a6f5b..fcd7ba61 100644 --- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs +++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs @@ -975,6 +975,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AllowCreateOrganization") + .HasColumnType("boolean"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone");