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-created-dialog/user-created-dialog.component.html deleted file mode 100644 index bc58a13..0000000 --- a/src/client/src/app/components/users/user-created-dialog/user-created-dialog.component.html +++ /dev/null @@ -1,34 +0,0 @@ - -
-
- - @if (user()?.loginToken; as loginToken) { - - } -
- -
-
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..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 @@ -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 c544cf0..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 @@ -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'; @@ -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), @@ -131,7 +131,7 @@ export class UserDialogComponent { .subscribe(({ type, response }) => { this.close(); if (type === addUserAction.success.type) { - this._userCreatedDialog().open(response); + this._userWelcomeDialog().open(response); } }); actions$ @@ -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; @@ -274,6 +285,13 @@ export class UserDialogComponent { return `${purpose}-${this._randomId}`; } + protected openUserWelcomeDialog() { + const user = this.userToUpdate(); + if (user) { + this._userWelcomeDialog().open(user); + } + } + private getUsersByIds(ids: string[]) { return ids.map(id => this._allUsers()[id]).filter(notNullish); } 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 new file mode 100644 index 0000000..3496cbf --- /dev/null +++ b/src/client/src/app/components/users/user-welcome-dialog/user-welcome-dialog.component.html @@ -0,0 +1,46 @@ + + +

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

+
+ @if (isLoading()) { +
+ +
+ } @else { +
+
+ + @if (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 51% 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..7263918 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 @@ -1,41 +1,68 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { interpolate, InterpolatePipe } from '@ngneers/signal-translate'; +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 { 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'; @Component({ - selector: 'app-user-created-dialog', + selector: 'app-user-welcome-dialog', standalone: true, - imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe], - templateUrl: './user-created-dialog.component.html', + imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe, ProgressSpinnerModule], + templateUrl: './user-welcome-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UserCreatedDialogComponent { +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); + private readonly _loadTokenState = selectSignal(selectUsersActionState('loadLoginToken')); + protected readonly isLoading = computed(() => isActionBusy(this._loadTokenState())); + protected readonly loginToken = selectSignal( + computed(() => selectUserLoginToken(this.user()?.id)) + ); + + protected loadLoginToken() { + if (this.loginToken()) { + return; + } + + const user = this.user(); + if (!user) return; + + this._store.dispatch(loadUserLoginTokenAction({ userId: user.id })); + } public open(user: User) { this.user.set(user); + this.loadLoginToken(); this.visible.set(true); } 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 943f5db..5eea8f6 100644 --- a/src/client/src/app/i18n/de.json +++ b/src/client/src/app/i18n/de.json @@ -195,7 +195,8 @@ "developer": "Entwickler" }, "error": { - "save": "Fehler beim Speichern des Benutzers." + "save": "Fehler beim Speichern des Benutzers.", + "exists": "Benutzer \"{{user}}\" existiert bereits" } }, "error": { @@ -209,8 +210,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 52ec1ef..8127df2 100644 --- a/src/client/src/app/i18n/en.json +++ b/src/client/src/app/i18n/en.json @@ -195,7 +195,8 @@ "developer": "Developer" }, "error": { - "save": "Failed to save user." + "save": "Failed to save user.", + "exists": "User \"{{user}}\" already exists" } }, "error": { @@ -209,8 +210,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", 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/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."); 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 c457801..89f46d3 100644 --- a/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs @@ -252,6 +252,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 521aa94..872d1fe 100644 --- a/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs @@ -252,6 +252,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 d3e202e..ad159ee 100644 --- a/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs @@ -233,6 +233,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Alias") + .IsUnique(); + b.HasIndex("LoginToken") .IsUnique();