From 2f3f538484ab8c4eb948808337ef751127a7f6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:50:24 +0200 Subject: [PATCH 1/8] feat: make user Alias unique on create - prevent accidentally double creation of a player --- .../Administration/Users/CreateUserEndpoint.cs | 18 +++++++++++++++++- src/server/host/Endpoints/EndpointErrors.cs | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/server/host/Endpoints/Administration/Users/CreateUserEndpoint.cs b/src/server/host/Endpoints/Administration/Users/CreateUserEndpoint.cs index 9f84f7b..11a7ed4 100644 --- a/src/server/host/Endpoints/Administration/Users/CreateUserEndpoint.cs +++ b/src/server/host/Endpoints/Administration/Users/CreateUserEndpoint.cs @@ -1,10 +1,14 @@ using System.ComponentModel.DataAnnotations; using FastEndpoints; using FluentValidation; +using Microsoft.EntityFrameworkCore; using MinigolfFriday.Data; using MinigolfFriday.Data.Entities; using MinigolfFriday.Domain.Models; using MinigolfFriday.Domain.Models.RealtimeEvents; +using MinigolfFriday.Host.Common; +using MinigolfFriday.Host.Mappers; +using MinigolfFriday.Host.Options; using MinigolfFriday.Host.Services; using MinigolfFriday.Host.Utilities; @@ -51,7 +55,8 @@ public CreateUserRequestValidator(IIdService idService) public class CreateUserEndpoint( DatabaseContext databaseContext, IRealtimeEventsService realtimeEventsService, - IIdService idService + IIdService idService, + IUserMapper userMapper ) : Endpoint { public override void Configure() @@ -59,10 +64,21 @@ public override void Configure() Post(""); Group(); Description(x => x.ClearDefaultProduces(200).Produces(201)); + this.ProducesError(EndpointErrors.UserExists); } public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct) { + var existentUser = await databaseContext + .Users.Where(x => x.Alias == req.Alias) + .Select(userMapper.MapUserExpression) + .FirstOrDefaultAsync(ct); + if (existentUser != null) + { + await this.SendErrorAsync(EndpointErrors.UserExists, existentUser.Alias, ct); + return; + } + var user = new UserEntity() { Alias = req.Alias, diff --git a/src/server/host/Endpoints/EndpointErrors.cs b/src/server/host/Endpoints/EndpointErrors.cs index 02de19a..fb226ed 100644 --- a/src/server/host/Endpoints/EndpointErrors.cs +++ b/src/server/host/Endpoints/EndpointErrors.cs @@ -6,6 +6,8 @@ public class EndpointErrors { public static readonly EndpointError.Params1 UserNotFound = new(404, "A user with the id {0} does not exist.", "UserId"); + public static readonly EndpointError.Params1 UserExists = + new(404, "A user with the alias {0} does already exist.", "Alias"); public static readonly EndpointError UserIdNotInClaims = new(403, "Could not extract user id from claims."); public static readonly EndpointError CannotDeleteSelf = new(409, "You cannot delete yourself."); From da9fdfdf6fa8ada3b9ced34728087197d1648bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:17:41 +0200 Subject: [PATCH 2/8] add warning on client side if user already exists --- .../users/user-dialog/user-dialog.component.ts | 13 ++++++++++++- src/client/src/app/i18n/de.json | 3 ++- src/client/src/app/i18n/en.json | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts index c544cf0..b3d34e5 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; -import { InterpolatePipe } from '@ngneers/signal-translate'; +import { interpolate, InterpolatePipe } from '@ngneers/signal-translate'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import copyToClipboard from 'copy-to-clipboard'; @@ -229,6 +229,17 @@ export class UserDialogComponent { } protected submit() { + if (Object.values(this._allUsers()).some(x => x?.alias === this.form.value.alias)) { + this._messageService.add({ + severity: 'error', + summary: interpolate(this.translations.users_dialog_error_exists(), { + user: this.form.value.alias, + }), + life: 2000, + }); + return; + } + if (!this.form.valid) { this.form.markAllAsTouched(); return; diff --git a/src/client/src/app/i18n/de.json b/src/client/src/app/i18n/de.json index 2fd014f..8c3105f 100644 --- a/src/client/src/app/i18n/de.json +++ b/src/client/src/app/i18n/de.json @@ -193,7 +193,8 @@ "developer": "Entwickler" }, "error": { - "save": "Fehler beim Speichern des Benutzers." + "save": "Fehler beim Speichern des Benutzers.", + "exists": "Benutzer \"{{user}}\" existiert bereits" } }, "error": { diff --git a/src/client/src/app/i18n/en.json b/src/client/src/app/i18n/en.json index 5ff6c52..46d49e0 100644 --- a/src/client/src/app/i18n/en.json +++ b/src/client/src/app/i18n/en.json @@ -193,7 +193,8 @@ "developer": "Developer" }, "error": { - "save": "Failed to save user." + "save": "Failed to save user.", + "exists": "User \"{{user}}\" already exists" } }, "error": { From e82c21a00283ea7829d821aaf423df6dc0b50eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:27:00 +0200 Subject: [PATCH 3/8] add 'open user-welcome-dialog' button to existing user --- .../user-dialog/user-dialog.component.html | 54 ++++++++++--------- .../user-dialog/user-dialog.component.ts | 36 ++++++++++--- .../user-welcome-dialog.component.html} | 12 +++-- .../user-welcome-dialog.component.ts} | 10 ++-- src/client/src/app/i18n/de.json | 4 +- src/client/src/app/i18n/en.json | 4 +- 6 files changed, 76 insertions(+), 44 deletions(-) rename src/client/src/app/components/users/{user-created-dialog/user-created-dialog.component.html => user-welcome-dialog/user-welcome-dialog.component.html} (66%) rename src/client/src/app/components/users/{user-created-dialog/user-created-dialog.component.ts => user-welcome-dialog/user-welcome-dialog.component.ts} (83%) diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.html b/src/client/src/app/components/users/user-dialog/user-dialog.component.html index b532764..6088b31 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.html +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.html @@ -29,30 +29,36 @@ @if (userToUpdate()) { - - @if (isTokenLoading()) { - - } @else if (!tokenVisible()) { - - } @else { - - } - + + @if (isTokenLoading()) { + + } @else if (!tokenVisible()) { + + } @else { + + } + + + + - - + }
@@ -193,4 +199,4 @@

{{ translations.users_dialog_roles_title() }}

- + diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts index b3d34e5..d58fe88 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts @@ -41,8 +41,8 @@ import { TranslateService } from '../../../services/translate.service'; import { areArraysEqual } from '../../../utils/array.utils'; import { notNullish } from '../../../utils/common.utils'; import { selectSignal } from '../../../utils/ngrx.utils'; -import { UserCreatedDialogComponent } from '../user-created-dialog/user-created-dialog.component'; import { UserItemComponent } from '../user-item/user-item.component'; +import { UserWelcomeDialogComponent } from '../user-welcome-dialog/user-welcome-dialog.component'; @Component({ selector: 'app-user-dialog', @@ -61,8 +61,8 @@ import { UserItemComponent } from '../user-item/user-item.component'; MessagesModule, OverlayPanelModule, ReactiveFormsModule, - UserCreatedDialogComponent, UserItemComponent, + UserWelcomeDialogComponent, ], templateUrl: './user-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -74,7 +74,7 @@ export class UserDialogComponent { private readonly _allUsers = selectSignal(userSelectors.selectEntities); private readonly _randomId = Math.random().toString(36).substring(2, 9); - private readonly _userCreatedDialog = viewChild.required(UserCreatedDialogComponent); + private readonly _userWelcomeDialog = viewChild.required(UserWelcomeDialogComponent); protected readonly form = this._formBuilder.group({ id: new FormControl(null), @@ -90,6 +90,7 @@ export class UserDialogComponent { protected readonly translations = inject(TranslateService).translations; protected readonly userToUpdate = signal(undefined); protected readonly visible = signal(false); + protected readonly userWelcomeDialogRequested = signal(false); private readonly _actionState = selectSignal( computed(() => selectUsersActionState(this.userToUpdate() ? 'update' : 'add')) @@ -131,12 +132,13 @@ export class UserDialogComponent { .subscribe(({ type, response }) => { this.close(); if (type === addUserAction.success.type) { - this._userCreatedDialog().open(response); + this._userWelcomeDialog().open(response); } }); - actions$ - .pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()) - .subscribe(() => this.tokenVisible.set(true)); + actions$.pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()).subscribe(() => { + this.tokenVisible.set(true); + this.openUserWelcomeDialog(); + }); effect( () => { @@ -285,6 +287,26 @@ export class UserDialogComponent { return `${purpose}-${this._randomId}`; } + protected requestUserWelcomeDialog() { + this.userWelcomeDialogRequested.set(true); + if (!this.tokenVisible()) { + this.loadLoginToken(); + return; + } + this.openUserWelcomeDialog(); + } + + protected openUserWelcomeDialog() { + if (this.userWelcomeDialogRequested() && this.userToUpdate()) { + const userToDisplay = { + loginToken: this.loginToken(), + alias: this.userToUpdate()?.alias, + } as User; + this._userWelcomeDialog().open(userToDisplay); + this.userWelcomeDialogRequested.set(false); + } + } + private getUsersByIds(ids: string[]) { return ids.map(id => this._allUsers()[id]).filter(notNullish); } diff --git a/src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.html b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html similarity index 66% rename from src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.html rename to src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html index bc58a13..9227a8f 100644 --- a/src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.html +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html @@ -1,24 +1,28 @@ + +

+ {{ translations.users_userWelcomeDialog_title() | interpolate: user() }} +

+
-
+
@if (user()?.loginToken; as loginToken) { } diff --git a/src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.ts b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts similarity index 83% rename from src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.ts rename to src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts index dcfd29d..0b9d11d 100644 --- a/src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.ts +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts @@ -10,13 +10,13 @@ import { User } from '../../../models/parsed-models'; import { TranslateService } from '../../../services/translate.service'; @Component({ - selector: 'app-user-created-dialog', + selector: 'app-user-welcome-dialog', standalone: true, imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe], - templateUrl: './user-created-dialog.component.html', + templateUrl: './user-welcome-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UserCreatedDialogComponent { +export class UserWelcomeDialogComponent { private readonly _messageService = inject(MessageService); protected readonly translations = inject(TranslateService).translations; @@ -29,13 +29,13 @@ export class UserCreatedDialogComponent { } protected copyWelcomeMessage() { - const message = interpolate(this.translations.users_userCreatedDialog_welcomeMessage(), { + const message = interpolate(this.translations.users_userWelcomeDialog_welcomeMessage(), { url: (document.head.querySelector('base') as HTMLBaseElement).href, }); copyToClipboard(message); this._messageService.add({ severity: 'success', - summary: this.translations.users_userCreatedDialog_welcomeMessageCopied(), + summary: this.translations.users_userWelcomeDialog_welcomeMessageCopied(), life: 2000, }); } diff --git a/src/client/src/app/i18n/de.json b/src/client/src/app/i18n/de.json index 8c3105f..4490b3a 100644 --- a/src/client/src/app/i18n/de.json +++ b/src/client/src/app/i18n/de.json @@ -208,8 +208,8 @@ "body": "Inhalt", "bodyDefault": "Benachrichtigungen funktionieren!!! Wohoo ⛳" }, - "userCreatedDialog": { - "title": "User \"{{alias}}\" created", + "userWelcomeDialog": { + "title": "Willkommen {{alias}}", "copyWelcomeMessage": "Willkommensnachricht kopieren", "copyPassword": "Passwort kopieren", "welcomeMessage": "Hallo Liebe Minigolffreunde!\nDie Anmeldung zum Minigolffreitag läuft ab jetzt über die Web-basierte App, welche du unter:\n{{url}}\naufrufen kannst.\nSpeichere sie dir als Lesezeichen, oder füge sie zu deinem Home-Bildschirm hinzu um jederzeit darauf zugreifen zu können.\nMelde dich dort bitte mit deinem Passwort an und registriere dich selber für deine Spielzeiten.\nBitte schreibe doch trotzdem im Facebook Beitrag, dass du dich registriert hast, damit dort die Konversation aufrecht erhalten wird.\nViel Spaß beim Minigolfen!\nDein Orga-Team", diff --git a/src/client/src/app/i18n/en.json b/src/client/src/app/i18n/en.json index 46d49e0..e652ba0 100644 --- a/src/client/src/app/i18n/en.json +++ b/src/client/src/app/i18n/en.json @@ -208,8 +208,8 @@ "body": "body", "bodyDefault": "Notifications are working!!! Wohoo ⛳" }, - "userCreatedDialog": { - "title": "User \"{{alias}}\" created", + "userWelcomeDialog": { + "title": "Welcome {{alias}}", "copyWelcomeMessage": "Copy welcome message", "copyPassword": "Copy password", "welcomeMessage": "Hello dear minigolf friends!\nRegistration for Minigolf Friday is now possible via the web-based app, which you can download at:\n{{url}}.\nSave it as a bookmark or add it to your home screen so that you can access it at any time.\nPlease log in there with your password and register yourself for your game times.\nPlease still write in the Facebook post that you have registered so that the conversation is maintained there.\nHave fun playing mini golf!\nYour organization team", From bdbe2f2b8ce6c8477190f175a196712b25cf394e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:35:05 +0200 Subject: [PATCH 4/8] Update src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html Co-authored-by: Marc Schmidt --- .../user-welcome-dialog/user-welcome-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html index 9227a8f..d956817 100644 --- a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html @@ -11,7 +11,7 @@

-
+
Date: Thu, 18 Jul 2024 13:04:14 +0200 Subject: [PATCH 5/8] laod login token inside Welcome dialog if necessary --- .../user-dialog/user-dialog.component.html | 2 +- .../user-dialog/user-dialog.component.ts | 26 ++++---------- .../user-welcome-dialog.component.html | 36 +++++++++++-------- .../user-welcome-dialog.component.ts | 36 +++++++++++++++++-- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.html b/src/client/src/app/components/users/user-dialog/user-dialog.component.html index 6088b31..f8b7c70 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.html +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.html @@ -56,7 +56,7 @@
} diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts index d58fe88..f6d19e8 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts @@ -135,10 +135,9 @@ export class UserDialogComponent { this._userWelcomeDialog().open(response); } }); - actions$.pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()).subscribe(() => { - this.tokenVisible.set(true); - this.openUserWelcomeDialog(); - }); + actions$ + .pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()) + .subscribe(() => this.tokenVisible.set(true)); effect( () => { @@ -287,23 +286,10 @@ export class UserDialogComponent { return `${purpose}-${this._randomId}`; } - protected requestUserWelcomeDialog() { - this.userWelcomeDialogRequested.set(true); - if (!this.tokenVisible()) { - this.loadLoginToken(); - return; - } - this.openUserWelcomeDialog(); - } - protected openUserWelcomeDialog() { - if (this.userWelcomeDialogRequested() && this.userToUpdate()) { - const userToDisplay = { - loginToken: this.loginToken(), - alias: this.userToUpdate()?.alias, - } as User; - this._userWelcomeDialog().open(userToDisplay); - this.userWelcomeDialogRequested.set(false); + const user = this.userToUpdate(); + if (user) { + this._userWelcomeDialog().open(user); } } diff --git a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html index d956817..3496cbf 100644 --- a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html @@ -10,23 +10,31 @@

{{ translations.users_userWelcomeDialog_title() | interpolate: user() }}

-
-
- - @if (user()?.loginToken; as loginToken) { + @if (isLoading()) { +
+ +
+ } @else { +
+
- } + @if (loginToken(); as loginToken) { + + } +
+ } + (onClick)="visible.set(false)" [text]="true" /> -
+ diff --git a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts index 0b9d11d..ab7d435 100644 --- a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts @@ -1,30 +1,62 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interpolate, InterpolatePipe } from '@ngneers/signal-translate'; +import { Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; import copyToClipboard from 'copy-to-clipboard'; import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { loadUserLoginTokenAction, selectUserLoginToken } from '../../../+state/users'; import { User } from '../../../models/parsed-models'; import { TranslateService } from '../../../services/translate.service'; +import { selectSignal } from '../../../utils/ngrx.utils'; @Component({ selector: 'app-user-welcome-dialog', standalone: true, - imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe], + imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe, ProgressSpinnerModule], templateUrl: './user-welcome-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserWelcomeDialogComponent { private readonly _messageService = inject(MessageService); + private readonly _store = inject(Store); protected readonly translations = inject(TranslateService).translations; protected readonly visible = signal(false); protected readonly user = signal(undefined); + protected readonly isLoading = signal(true); + protected readonly loginToken = selectSignal( + computed(() => selectUserLoginToken(this.user()?.id)) + ); + + constructor() { + const actions$ = inject(Actions); + actions$.pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()).subscribe(() => { + this.isLoading.set(false); + }); + } + + protected loadLoginToken() { + if (this.loginToken()) { + this.isLoading.set(false); + return; + } + + const user = this.user(); + if (!user) return; + + this._store.dispatch(loadUserLoginTokenAction({ userId: user.id })); + } public open(user: User) { this.user.set(user); + this.isLoading.set(true); + this.loadLoginToken(); this.visible.set(true); } From a594e4ac1728273b8b81138e12925e729c4b7b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:05:20 +0200 Subject: [PATCH 6/8] remove unused signal --- .../app/components/users/user-dialog/user-dialog.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts index f6d19e8..5e7ebc1 100644 --- a/src/client/src/app/components/users/user-dialog/user-dialog.component.ts +++ b/src/client/src/app/components/users/user-dialog/user-dialog.component.ts @@ -90,7 +90,6 @@ export class UserDialogComponent { protected readonly translations = inject(TranslateService).translations; protected readonly userToUpdate = signal(undefined); protected readonly visible = signal(false); - protected readonly userWelcomeDialogRequested = signal(false); private readonly _actionState = selectSignal( computed(() => selectUsersActionState(this.userToUpdate() ? 'update' : 'add')) From 3d3eb9b76bf7ce25ae44e513f1196603b2322694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:23:35 +0200 Subject: [PATCH 7/8] add unique constrain to User Alias in Database --- src/server/data/Entities/UserEntity.cs | 1 + ...40718112149_UserHasUniqueAlias.Designer.cs | 616 ++++++++++++++++++ .../20240718112149_UserHasUniqueAlias.cs | 29 + .../DatabaseContextModelSnapshot.cs | 4 + ...40718112151_UserHasUniqueAlias.Designer.cs | 614 +++++++++++++++++ .../20240718112151_UserHasUniqueAlias.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...40718112146_UserHasUniqueAlias.Designer.cs | 591 +++++++++++++++++ .../20240718112146_UserHasUniqueAlias.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + 10 files changed, 1917 insertions(+) create mode 100644 src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.Designer.cs create mode 100644 src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.cs create mode 100644 src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.Designer.cs create mode 100644 src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.cs create mode 100644 src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.Designer.cs create mode 100644 src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.cs diff --git a/src/server/data/Entities/UserEntity.cs b/src/server/data/Entities/UserEntity.cs index 7a09ed0..523b989 100644 --- a/src/server/data/Entities/UserEntity.cs +++ b/src/server/data/Entities/UserEntity.cs @@ -58,5 +58,6 @@ public static void Configure(EntityTypeBuilder builder) builder.HasKey(x => x.Id); builder.HasIndex(x => x.LoginToken).IsUnique(); + builder.HasIndex(x => x.Alias).IsUnique(); } } diff --git a/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.Designer.cs b/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.Designer.cs new file mode 100644 index 0000000..5f4367b --- /dev/null +++ b/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.Designer.cs @@ -0,0 +1,616 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MinigolfFriday.Data; + +#nullable disable + +namespace MinigolfFriday.Migrations.MsSql.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240718112149_UserHasUniqueAlias")] + partial class UserHasUniqueAlias + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date"); + + b.Property("RegistrationDeadline") + .HasColumnType("datetimeoffset") + .HasColumnName("registration_deadline"); + + b.Property("Staged") + .HasColumnType("bit") + .HasColumnName("staged"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("started_at"); + + b.HasKey("Id"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("group_code"); + + b.Property("timeslot_id") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("timeslot_id"); + + b.ToTable("event_instances", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("event_timeslot_id") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("event_timeslot_id"); + + b.ToTable("event_instance_preconfigurations", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EventId") + .HasColumnType("bigint") + .HasColumnName("event_id"); + + b.Property("IsFallbackAllowed") + .HasColumnType("bit") + .HasColumnName("is_fallback_allowed"); + + b.Property("MapId") + .HasColumnType("bigint") + .HasColumnName("map_id"); + + b.Property("Time") + .HasColumnType("time") + .HasColumnName("time"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.HasIndex("EventId", "Time") + .IsUnique(); + + b.ToTable("event_timeslots", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EventTimeslotId") + .HasColumnType("bigint") + .HasColumnName("event_timeslot_id"); + + b.Property("FallbackEventTimeslotId") + .HasColumnType("bigint") + .HasColumnName("fallback_event_timeslot_id"); + + b.Property("PlayerId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("EventTimeslotId"); + + b.HasIndex("FallbackEventTimeslotId"); + + b.HasIndex("PlayerId"); + + b.ToTable("event_timeslot_registration", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true) + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("maps", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.RoleEntity", b => + { + b.Property("Id") + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("roles", (string)null); + + b.HasData( + new + { + Id = 0, + Name = "Player" + }, + new + { + Id = 1, + Name = "Admin" + }, + new + { + Id = 2, + Name = "Developer" + }); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Alias") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)") + .HasColumnName("alias"); + + b.Property("LoginToken") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("login_token"); + + b.Property("SettingsId") + .HasColumnType("bigint") + .HasColumnName("settings_id"); + + b.HasKey("Id"); + + b.HasIndex("Alias") + .IsUnique() + .HasFilter("[alias] IS NOT NULL"); + + b.HasIndex("LoginToken") + .IsUnique() + .HasFilter("[login_token] IS NOT NULL"); + + b.HasIndex("SettingsId"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Auth") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("auth"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("endpoint"); + + b.Property("Lang") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasColumnName("lang"); + + b.Property("P256DH") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("p256dh"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("user_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EnableNotifications") + .HasColumnType("bit") + .HasColumnName("enable_notifications"); + + b.Property("NotifyOnEventPublish") + .HasColumnType("bit") + .HasColumnName("notify_on_event_publish"); + + b.Property("NotifyOnEventStart") + .HasColumnType("bit") + .HasColumnName("notify_on_event_start"); + + b.Property("NotifyOnEventUpdated") + .HasColumnType("bit") + .HasColumnName("notify_on_event_updated"); + + b.Property("NotifyOnTimeslotStart") + .HasColumnType("bit") + .HasColumnName("notify_on_timeslot_start"); + + b.Property("SecondsToNotifyBeforeTimeslotStart") + .HasColumnType("int") + .HasColumnName("seconds_to_notify_before_timeslot_start"); + + b.HasKey("Id"); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.Property("event_instance_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("event_instance_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("event_instances_to_users"); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.Property("avoided_user_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("avoided_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_avoided_users"); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.Property("event_instance_preconfiguration_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("event_instance_preconfiguration_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_event_instance_preconfigurations"); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.Property("preferred_user_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("preferred_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_preferred_users"); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.Property("role_id") + .HasColumnType("int"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("role_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_roles"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Instances") + .HasForeignKey("timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeSlot") + .WithMany("Preconfigurations") + .HasForeignKey("event_timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeSlot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventEntity", "Event") + .WithMany("Timeslots") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.MinigolfMapEntity", "Map") + .WithMany("EventTimeslots") + .HasForeignKey("MapId"); + + b.Navigation("Event"); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Registrations") + .HasForeignKey("EventTimeslotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "FallbackEventTimeslot") + .WithMany() + .HasForeignKey("FallbackEventTimeslotId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + + b.Navigation("FallbackEventTimeslot"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserSettingsEntity", "Settings") + .WithMany("Users") + .HasForeignKey("SettingsId"); + + b.Navigation("Settings"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "User") + .WithMany("PushSubscriptions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstanceEntity", null) + .WithMany() + .HasForeignKey("event_instance_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("avoided_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", null) + .WithMany() + .HasForeignKey("event_instance_preconfiguration_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("preferred_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.HasOne("MinigolfFriday.Data.Entities.RoleEntity", null) + .WithMany() + .HasForeignKey("role_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Navigation("Timeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Navigation("Instances"); + + b.Navigation("Preconfigurations"); + + b.Navigation("Registrations"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Navigation("EventTimeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Navigation("PushSubscriptions"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.cs b/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.cs new file mode 100644 index 0000000..3af15c5 --- /dev/null +++ b/src/server/migrations/mssql/Migrations/20240718112149_UserHasUniqueAlias.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.MsSql.Migrations +{ + /// + public partial class UserHasUniqueAlias : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_users_alias", + table: "users", + column: "alias", + unique: true, + filter: "[alias] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_users_alias", + table: "users"); + } + } +} diff --git a/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs index 228ef5e..a3eaeca 100644 --- a/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs @@ -248,6 +248,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Alias") + .IsUnique() + .HasFilter("[alias] IS NOT NULL"); + b.HasIndex("LoginToken") .IsUnique() .HasFilter("[login_token] IS NOT NULL"); diff --git a/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.Designer.cs b/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.Designer.cs new file mode 100644 index 0000000..d4f7a46 --- /dev/null +++ b/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.Designer.cs @@ -0,0 +1,614 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MinigolfFriday.Data; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MinigolfFriday.Migrations.PostgreSql.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240718112151_UserHasUniqueAlias")] + partial class UserHasUniqueAlias + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date"); + + b.Property("RegistrationDeadline") + .HasColumnType("timestamp with time zone") + .HasColumnName("registration_deadline"); + + b.Property("Staged") + .HasColumnType("boolean") + .HasColumnName("staged"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.HasKey("Id"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("group_code"); + + b.Property("timeslot_id") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("timeslot_id"); + + b.ToTable("event_instances", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("event_timeslot_id") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("event_timeslot_id"); + + b.ToTable("event_instance_preconfigurations", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EventId") + .HasColumnType("bigint") + .HasColumnName("event_id"); + + b.Property("IsFallbackAllowed") + .HasColumnType("boolean") + .HasColumnName("is_fallback_allowed"); + + b.Property("MapId") + .HasColumnType("bigint") + .HasColumnName("map_id"); + + b.Property("Time") + .HasColumnType("time without time zone") + .HasColumnName("time"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.HasIndex("EventId", "Time") + .IsUnique(); + + b.ToTable("event_timeslots", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EventTimeslotId") + .HasColumnType("bigint") + .HasColumnName("event_timeslot_id"); + + b.Property("FallbackEventTimeslotId") + .HasColumnType("bigint") + .HasColumnName("fallback_event_timeslot_id"); + + b.Property("PlayerId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("EventTimeslotId"); + + b.HasIndex("FallbackEventTimeslotId"); + + b.HasIndex("PlayerId"); + + b.ToTable("event_timeslot_registration", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("maps", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.RoleEntity", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("roles", (string)null); + + b.HasData( + new + { + Id = 0, + Name = "Player" + }, + new + { + Id = 1, + Name = "Admin" + }, + new + { + Id = 2, + Name = "Developer" + }); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Alias") + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("alias"); + + b.Property("LoginToken") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("login_token"); + + b.Property("SettingsId") + .HasColumnType("bigint") + .HasColumnName("settings_id"); + + b.HasKey("Id"); + + b.HasIndex("Alias") + .IsUnique(); + + b.HasIndex("LoginToken") + .IsUnique(); + + b.HasIndex("SettingsId"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("auth"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("endpoint"); + + b.Property("Lang") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("lang"); + + b.Property("P256DH") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("p256dh"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("user_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EnableNotifications") + .HasColumnType("boolean") + .HasColumnName("enable_notifications"); + + b.Property("NotifyOnEventPublish") + .HasColumnType("boolean") + .HasColumnName("notify_on_event_publish"); + + b.Property("NotifyOnEventStart") + .HasColumnType("boolean") + .HasColumnName("notify_on_event_start"); + + b.Property("NotifyOnEventUpdated") + .HasColumnType("boolean") + .HasColumnName("notify_on_event_updated"); + + b.Property("NotifyOnTimeslotStart") + .HasColumnType("boolean") + .HasColumnName("notify_on_timeslot_start"); + + b.Property("SecondsToNotifyBeforeTimeslotStart") + .HasColumnType("integer") + .HasColumnName("seconds_to_notify_before_timeslot_start"); + + b.HasKey("Id"); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.Property("event_instance_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("event_instance_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("event_instances_to_users"); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.Property("avoided_user_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("avoided_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_avoided_users"); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.Property("event_instance_preconfiguration_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("event_instance_preconfiguration_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_event_instance_preconfigurations"); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.Property("preferred_user_id") + .HasColumnType("bigint"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("preferred_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_preferred_users"); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.Property("role_id") + .HasColumnType("integer"); + + b.Property("user_id") + .HasColumnType("bigint"); + + b.HasKey("role_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_roles"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Instances") + .HasForeignKey("timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeSlot") + .WithMany("Preconfigurations") + .HasForeignKey("event_timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeSlot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventEntity", "Event") + .WithMany("Timeslots") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.MinigolfMapEntity", "Map") + .WithMany("EventTimeslots") + .HasForeignKey("MapId"); + + b.Navigation("Event"); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Registrations") + .HasForeignKey("EventTimeslotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "FallbackEventTimeslot") + .WithMany() + .HasForeignKey("FallbackEventTimeslotId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + + b.Navigation("FallbackEventTimeslot"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserSettingsEntity", "Settings") + .WithMany("Users") + .HasForeignKey("SettingsId"); + + b.Navigation("Settings"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "User") + .WithMany("PushSubscriptions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstanceEntity", null) + .WithMany() + .HasForeignKey("event_instance_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("avoided_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", null) + .WithMany() + .HasForeignKey("event_instance_preconfiguration_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("preferred_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.HasOne("MinigolfFriday.Data.Entities.RoleEntity", null) + .WithMany() + .HasForeignKey("role_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Navigation("Timeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Navigation("Instances"); + + b.Navigation("Preconfigurations"); + + b.Navigation("Registrations"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Navigation("EventTimeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Navigation("PushSubscriptions"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.cs b/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.cs new file mode 100644 index 0000000..7d12b5b --- /dev/null +++ b/src/server/migrations/postgresql/Migrations/20240718112151_UserHasUniqueAlias.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.PostgreSql.Migrations +{ + /// + public partial class UserHasUniqueAlias : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_users_alias", + table: "users", + column: "alias", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_users_alias", + table: "users"); + } + } +} diff --git a/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs index cb29ae0..1e1eb74 100644 --- a/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs @@ -248,6 +248,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Alias") + .IsUnique(); + b.HasIndex("LoginToken") .IsUnique(); diff --git a/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.Designer.cs b/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.Designer.cs new file mode 100644 index 0000000..b3a2639 --- /dev/null +++ b/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.Designer.cs @@ -0,0 +1,591 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MinigolfFriday.Data; + +#nullable disable + +namespace MinigolfFriday.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240718112146_UserHasUniqueAlias")] + partial class UserHasUniqueAlias + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("RegistrationDeadline") + .HasColumnType("TEXT") + .HasColumnName("registration_deadline"); + + b.Property("Staged") + .HasColumnType("INTEGER") + .HasColumnName("staged"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.HasKey("Id"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("group_code"); + + b.Property("timeslot_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("timeslot_id"); + + b.ToTable("event_instances", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("event_timeslot_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("event_timeslot_id"); + + b.ToTable("event_instance_preconfigurations", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("EventId") + .HasColumnType("INTEGER") + .HasColumnName("event_id"); + + b.Property("IsFallbackAllowed") + .HasColumnType("INTEGER") + .HasColumnName("is_fallback_allowed"); + + b.Property("MapId") + .HasColumnType("INTEGER") + .HasColumnName("map_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.HasIndex("EventId", "Time") + .IsUnique(); + + b.ToTable("event_timeslots", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("EventTimeslotId") + .HasColumnType("INTEGER") + .HasColumnName("event_timeslot_id"); + + b.Property("FallbackEventTimeslotId") + .HasColumnType("INTEGER") + .HasColumnName("fallback_event_timeslot_id"); + + b.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("EventTimeslotId"); + + b.HasIndex("FallbackEventTimeslotId"); + + b.HasIndex("PlayerId"); + + b.ToTable("event_timeslot_registration", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("maps", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.RoleEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("roles", (string)null); + + b.HasData( + new + { + Id = 0, + Name = "Player" + }, + new + { + Id = 1, + Name = "Admin" + }, + new + { + Id = 2, + Name = "Developer" + }); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Alias") + .HasMaxLength(150) + .HasColumnType("TEXT") + .HasColumnName("alias"); + + b.Property("LoginToken") + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("login_token"); + + b.Property("SettingsId") + .HasColumnType("INTEGER") + .HasColumnName("settings_id"); + + b.HasKey("Id"); + + b.HasIndex("Alias") + .IsUnique(); + + b.HasIndex("LoginToken") + .IsUnique(); + + b.HasIndex("SettingsId"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Auth") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("auth"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT") + .HasColumnName("endpoint"); + + b.Property("Lang") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("lang"); + + b.Property("P256DH") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("p256dh"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("user_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("EnableNotifications") + .HasColumnType("INTEGER") + .HasColumnName("enable_notifications"); + + b.Property("NotifyOnEventPublish") + .HasColumnType("INTEGER") + .HasColumnName("notify_on_event_publish"); + + b.Property("NotifyOnEventStart") + .HasColumnType("INTEGER") + .HasColumnName("notify_on_event_start"); + + b.Property("NotifyOnEventUpdated") + .HasColumnType("INTEGER") + .HasColumnName("notify_on_event_updated"); + + b.Property("NotifyOnTimeslotStart") + .HasColumnType("INTEGER") + .HasColumnName("notify_on_timeslot_start"); + + b.Property("SecondsToNotifyBeforeTimeslotStart") + .HasColumnType("INTEGER") + .HasColumnName("seconds_to_notify_before_timeslot_start"); + + b.HasKey("Id"); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.Property("event_instance_id") + .HasColumnType("INTEGER"); + + b.Property("user_id") + .HasColumnType("INTEGER"); + + b.HasKey("event_instance_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("event_instances_to_users"); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.Property("avoided_user_id") + .HasColumnType("INTEGER"); + + b.Property("user_id") + .HasColumnType("INTEGER"); + + b.HasKey("avoided_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_avoided_users"); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.Property("event_instance_preconfiguration_id") + .HasColumnType("INTEGER"); + + b.Property("user_id") + .HasColumnType("INTEGER"); + + b.HasKey("event_instance_preconfiguration_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_event_instance_preconfigurations"); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.Property("preferred_user_id") + .HasColumnType("INTEGER"); + + b.Property("user_id") + .HasColumnType("INTEGER"); + + b.HasKey("preferred_user_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_preferred_users"); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.Property("role_id") + .HasColumnType("INTEGER"); + + b.Property("user_id") + .HasColumnType("INTEGER"); + + b.HasKey("role_id", "user_id"); + + b.HasIndex("user_id"); + + b.ToTable("users_to_roles"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Instances") + .HasForeignKey("timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeSlot") + .WithMany("Preconfigurations") + .HasForeignKey("event_timeslot_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeSlot"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventEntity", "Event") + .WithMany("Timeslots") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.MinigolfMapEntity", "Map") + .WithMany("EventTimeslots") + .HasForeignKey("MapId"); + + b.Navigation("Event"); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotRegistrationEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") + .WithMany("Registrations") + .HasForeignKey("EventTimeslotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "FallbackEventTimeslot") + .WithMany() + .HasForeignKey("FallbackEventTimeslotId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventTimeslot"); + + b.Navigation("FallbackEventTimeslot"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserSettingsEntity", "Settings") + .WithMany("Users") + .HasForeignKey("SettingsId"); + + b.Navigation("Settings"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserPushSubscriptionEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", "User") + .WithMany("PushSubscriptions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("event_instances_to_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstanceEntity", null) + .WithMany() + .HasForeignKey("event_instance_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_avoided_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("avoided_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_event_instance_preconfigurations", b => + { + b.HasOne("MinigolfFriday.Data.Entities.EventInstancePreconfigurationEntity", null) + .WithMany() + .HasForeignKey("event_instance_preconfiguration_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_preferred_users", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("preferred_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("users_to_roles", b => + { + b.HasOne("MinigolfFriday.Data.Entities.RoleEntity", null) + .WithMany() + .HasForeignKey("role_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.Navigation("Timeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventTimeslotEntity", b => + { + b.Navigation("Instances"); + + b.Navigation("Preconfigurations"); + + b.Navigation("Registrations"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.MinigolfMapEntity", b => + { + b.Navigation("EventTimeslots"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserEntity", b => + { + b.Navigation("PushSubscriptions"); + }); + + modelBuilder.Entity("MinigolfFriday.Data.Entities.UserSettingsEntity", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.cs b/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.cs new file mode 100644 index 0000000..aebe127 --- /dev/null +++ b/src/server/migrations/sqlite/Migrations/20240718112146_UserHasUniqueAlias.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.Sqlite.Migrations +{ + /// + public partial class UserHasUniqueAlias : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_users_alias", + table: "users", + column: "alias", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_users_alias", + table: "users"); + } + } +} diff --git a/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs index 42cdd2b..f8c8638 100644 --- a/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs @@ -229,6 +229,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Alias") + .IsUnique(); + b.HasIndex("LoginToken") .IsUnique(); From 45d271d9ac2d33788b9b6d3d8384a87d660825b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schmidt?= <9435005+AnSch1510@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:20:06 +0200 Subject: [PATCH 8/8] select loading state from global state --- .../user-welcome-dialog.component.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts index ab7d435..7263918 100644 --- a/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.ts @@ -1,8 +1,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interpolate, InterpolatePipe } from '@ngneers/signal-translate'; -import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import copyToClipboard from 'copy-to-clipboard'; import { MessageService } from 'primeng/api'; @@ -10,7 +8,12 @@ import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; -import { loadUserLoginTokenAction, selectUserLoginToken } from '../../../+state/users'; +import { isActionBusy } from '../../../+state/action-state'; +import { + loadUserLoginTokenAction, + selectUserLoginToken, + selectUsersActionState, +} from '../../../+state/users'; import { User } from '../../../models/parsed-models'; import { TranslateService } from '../../../services/translate.service'; import { selectSignal } from '../../../utils/ngrx.utils'; @@ -29,21 +32,14 @@ export class UserWelcomeDialogComponent { protected readonly visible = signal(false); protected readonly user = signal(undefined); - protected readonly isLoading = signal(true); + private readonly _loadTokenState = selectSignal(selectUsersActionState('loadLoginToken')); + protected readonly isLoading = computed(() => isActionBusy(this._loadTokenState())); protected readonly loginToken = selectSignal( computed(() => selectUserLoginToken(this.user()?.id)) ); - constructor() { - const actions$ = inject(Actions); - actions$.pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()).subscribe(() => { - this.isLoading.set(false); - }); - } - protected loadLoginToken() { if (this.loginToken()) { - this.isLoading.set(false); return; } @@ -55,7 +51,6 @@ export class UserWelcomeDialogComponent { public open(user: User) { this.user.set(user); - this.isLoading.set(true); this.loadLoginToken(); this.visible.set(true); }