diff --git a/src/client/src/app/+state/events/actions/set-editing-event-instances.action.ts b/src/client/src/app/+state/events/actions/set-editing-event-instances.action.ts new file mode 100644 index 0000000..25ddde9 --- /dev/null +++ b/src/client/src/app/+state/events/actions/set-editing-event-instances.action.ts @@ -0,0 +1,57 @@ +import { inject } from '@angular/core'; +import { on } from '@ngrx/store'; +import { produce } from 'immer'; +import { switchMap } from 'rxjs'; + +import { EventAdministrationService } from '../../../api/services'; +import { AuthService } from '../../../services/auth.service'; +import { createHttpAction, handleHttpAction, onHttpAction } from '../../action-state'; +import { createFunctionalEffect } from '../../functional-effect'; +import { Effects, Reducers } from '../../utils'; +import { EVENTS_ACTION_SCOPE } from '../consts'; +import { eventEntityAdapter, EventsFeatureState } from '../events.state'; + +export const setEditingEventInstancesAction = createHttpAction< + { eventId: string; isEditing: boolean }, + { userIdEditingInstances: string | null } +>()(EVENTS_ACTION_SCOPE, 'Set Editing Event Instances'); + +export const setEditingEventInstancesReducers: Reducers = [ + on(setEditingEventInstancesAction.success, (state, { props, response }) => + eventEntityAdapter.mapOne( + { + id: props.eventId, + map: produce(draft => { + draft.userIdEditingInstances = response.userIdEditingInstances; + }), + }, + state + ) + ), + handleHttpAction('setInstancesEditing', setEditingEventInstancesAction), +]; + +export const setEditingEventInstancesEffects: Effects = { + setEditingEventInstances$: createFunctionalEffect.dispatching( + (api = inject(EventAdministrationService), authService = inject(AuthService)) => + onHttpAction(setEditingEventInstancesAction).pipe( + switchMap(({ props }) => setEditingEventInstances(api, props, authService)) + ) + ), +}; + +async function setEditingEventInstances( + api: EventAdministrationService, + props: ReturnType['props'], + authService: AuthService +) { + const response = await api.setEventInstancesEditing({ + eventId: props.eventId, + body: { isEditing: props.isEditing }, + }); + return response.ok + ? setEditingEventInstancesAction.success(props, { + userIdEditingInstances: props.isEditing ? (authService.user()?.id ?? null) : null, + }) + : setEditingEventInstancesAction.error(props, response); +} diff --git a/src/client/src/app/+state/events/events.actions.ts b/src/client/src/app/+state/events/events.actions.ts index 8b69982..c53a1dd 100644 --- a/src/client/src/app/+state/events/events.actions.ts +++ b/src/client/src/app/+state/events/events.actions.ts @@ -11,6 +11,7 @@ export { removeEventTimeslotAction } from './actions/remove-event-timeslot.actio export { removeEventAction } from './actions/remove-event.action'; export { removePlayerFromPreconfigAction } from './actions/remove-player-from-preconfig.action'; export { resetEventsActionStateAction } from './actions/reset-events-action-state.action'; +export { setEditingEventInstancesAction } from './actions/set-editing-event-instances.action'; export { setEventInstancesAction } from './actions/set-event-instances.action'; export { startEventAction } from './actions/start-event.action'; export { updateEventTimeslotAction } from './actions/update-event-timeslot.action'; diff --git a/src/client/src/app/+state/events/events.effects.ts b/src/client/src/app/+state/events/events.effects.ts index 24162ce..2e3b302 100644 --- a/src/client/src/app/+state/events/events.effects.ts +++ b/src/client/src/app/+state/events/events.effects.ts @@ -12,6 +12,10 @@ import { removeEventPreconfigEffects } from './actions/remove-event-preconfig.ac import { removeEventTimeslotEffects } from './actions/remove-event-timeslot.action'; import { removeEventAction, removeEventEffects } from './actions/remove-event.action'; import { removePlayerFromPreconfigEffects } from './actions/remove-player-from-preconfig.action'; +import { + setEditingEventInstancesAction, + setEditingEventInstancesEffects, +} from './actions/set-editing-event-instances.action'; import { setEventInstancesEffects } from './actions/set-event-instances.action'; import { startEventEffects } from './actions/start-event.action'; import { updateEventTimeslotEffects } from './actions/update-event-timeslot.action'; @@ -36,6 +40,7 @@ export const eventsFeatureEffects: Effects[] = [ removeEventTimeslotEffects, removeEventEffects, removePlayerFromPreconfigEffects, + setEditingEventInstancesEffects, setEventInstancesEffects, startEventEffects, updateEventEffects, @@ -68,6 +73,16 @@ export const eventsFeatureEffects: Effects[] = [ map(event => eventTimeslotRegistrationChangedAction(event)) ) ), + eventInstancesEditorChanged$: createFunctionalEffect.dispatching(() => + inject(RealtimeEventsService).eventInstancesEditorChanged.pipe( + map(event => + setEditingEventInstancesAction.success( + { eventId: event.eventId, isEditing: !!event.userId }, + { userIdEditingInstances: event.userId ?? null } + ) + ) + ) + ), onServerReconnected$: createFunctionalEffect.dispatching(() => inject(RealtimeEventsService).onReconnected$.pipe( diff --git a/src/client/src/app/+state/events/events.reducer.ts b/src/client/src/app/+state/events/events.reducer.ts index d502ed7..0e20837 100644 --- a/src/client/src/app/+state/events/events.reducer.ts +++ b/src/client/src/app/+state/events/events.reducer.ts @@ -13,6 +13,7 @@ import { removeEventTimeslotReducers } from './actions/remove-event-timeslot.act import { removeEventReducers } from './actions/remove-event.action'; import { removePlayerFromPreconfigReducers } from './actions/remove-player-from-preconfig.action'; import { resetEventsActionStateReducers } from './actions/reset-events-action-state.action'; +import { setEditingEventInstancesReducers } from './actions/set-editing-event-instances.action'; import { setEventInstancesReducers } from './actions/set-event-instances.action'; import { startEventReducers } from './actions/start-event.action'; import { updateEventTimeslotReducers } from './actions/update-event-timeslot.action'; @@ -35,6 +36,7 @@ export const eventsReducer = createReducer( ...removeEventReducers, ...removePlayerFromPreconfigReducers, ...resetEventsActionStateReducers, + ...setEditingEventInstancesReducers, ...setEventInstancesReducers, ...startEventReducers, ...updateEventTimeslotReducers, diff --git a/src/client/src/app/+state/events/events.selectors.ts b/src/client/src/app/+state/events/events.selectors.ts index 5b0bc62..dd1f7fc 100644 --- a/src/client/src/app/+state/events/events.selectors.ts +++ b/src/client/src/app/+state/events/events.selectors.ts @@ -23,7 +23,13 @@ export function selectEventsActionState(action: keyof EventsFeatureState['action export function selectEvent(id: string | null | undefined) { return createDistinctSelector(selectEventsFeature, state => - id ? state.entities[id] ?? null : null + id ? (state.entities[id] ?? null) : null + ); +} + +export function selectEventEditor(id: string | null | undefined) { + return createDistinctSelector(selectEventsFeature, state => + id ? (state.entities[id]?.userIdEditingInstances ?? null) : null ); } @@ -33,7 +39,7 @@ export function selectEventTimeslot( ) { return createDistinctSelector(selectEventsFeature, state => eventId && timeslotId - ? state.entities[eventId]?.timeslots.find(x => x.id === timeslotId) ?? null + ? (state.entities[eventId]?.timeslots.find(x => x.id === timeslotId) ?? null) : null ); } diff --git a/src/client/src/app/+state/events/events.state.ts b/src/client/src/app/+state/events/events.state.ts index 78748c7..802ffec 100644 --- a/src/client/src/app/+state/events/events.state.ts +++ b/src/client/src/app/+state/events/events.state.ts @@ -16,6 +16,7 @@ export type EventsFeatureState = EntityState & { removeTimeslot: ActionState; buildInstances: ActionState; setInstances: ActionState; + setInstancesEditing: ActionState; updateTimeslot: ActionState; addPreconfig: ActionState; removePreconfig: ActionState; @@ -42,6 +43,7 @@ export const initialEventsFeatureState: EventsFeatureState = eventEntityAdapter. removeTimeslot: initialActionState, buildInstances: initialActionState, setInstances: initialActionState, + setInstancesEditing: initialActionState, updateTimeslot: initialActionState, addPreconfig: initialActionState, removePreconfig: initialActionState, diff --git a/src/client/src/app/components/events/event-details/event-details.component.html b/src/client/src/app/components/events/event-details/event-details.component.html index 31b9dd8..8e791f8 100644 --- a/src/client/src/app/components/events/event-details/event-details.component.html +++ b/src/client/src/app/components/events/event-details/event-details.component.html @@ -103,9 +103,9 @@

{{ translations.events_facebookLink() }}

{{ translations.events_timeslots() }}

- {{ playersAmount() }} + {{ playersAmount() }} @if (!event.startedAt && event.staged) { {{ translations.events_groups() }} }
+ @if (event.userIdEditingInstances) { +
+ + {{ allUsers()[event.userIdEditingInstances]?.alias }} +
+ }
@for (timeslot of timeslots(); track timeslot.id) {
diff --git a/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts index e7d8bde..902d88b 100644 --- a/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts +++ b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts @@ -1,20 +1,35 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { produce } from 'immer'; +import { ConfirmationService, MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { InputGroupModule } from 'primeng/inputgroup'; import { ListboxModule } from 'primeng/listbox'; import { OverlayPanelModule } from 'primeng/overlaypanel'; -import { filter } from 'rxjs'; +import { filter, firstValueFrom, pairwise } from 'rxjs'; -import { isActionBusy } from '../../../+state/action-state'; -import { selectEventsActionState, setEventInstancesAction } from '../../../+state/events'; +import { hasActionFailed, isActionBusy, isActionIdle } from '../../../+state/action-state'; +import { + selectEventEditor, + selectEventsActionState, + setEditingEventInstancesAction, + setEventInstancesAction, +} from '../../../+state/events'; import { userSelectors } from '../../../+state/users'; import { Event, EventInstance, EventTimeslot } from '../../../models/parsed-models'; +import { AuthService } from '../../../services/auth.service'; import { Logger } from '../../../services/logger.service'; import { TranslateService } from '../../../services/translate.service'; import { notNullish } from '../../../utils/common.utils'; @@ -40,6 +55,9 @@ type EventInstances = { timeslot: EventTimeslot; instances: EventInstance[] }[]; }) export class EventInstancesDialogComponent { private readonly _store = inject(Store); + private readonly _messageService = inject(MessageService); + private readonly _confirmService = inject(ConfirmationService); + private readonly _authService = inject(AuthService); protected readonly translations = inject(TranslateService).translations; private readonly _removeItem: EventInstance = { @@ -51,6 +69,9 @@ export class EventInstancesDialogComponent { protected readonly visible = signal(false); protected readonly event = signal(null); + protected readonly eventEditedBy = selectSignal( + computed(() => selectEventEditor(this.event()?.id)) + ); protected readonly instances = signal([]); protected readonly allUsers = selectSignal(userSelectors.selectEntities); protected readonly unassignedUsers = computed(() => @@ -102,6 +123,40 @@ export class EventInstancesDialogComponent { protected readonly isBusy = computed(() => isActionBusy(this._actionState())); constructor() { + toObservable(this.visible) + .pipe( + pairwise(), + takeUntilDestroyed(), + filter(([prev, curr]) => prev && !curr) + ) + .subscribe(() => { + const event = this.event(); + if (event && this.eventEditedBy() === this._authService.user()?.id) { + this._store.dispatch( + setEditingEventInstancesAction({ eventId: event.id, isEditing: false }) + ); + } + this.event.set(null); + this.instances.set([]); + }); + + effect( + () => { + const eventEditedBy = this.eventEditedBy(); + if (eventEditedBy !== this._authService.user()?.id && untracked(() => this.visible())) { + this.visible.set(false); + const user = eventEditedBy ? this.allUsers()[eventEditedBy] : null; + this._confirmService.confirm({ + message: this.translations.events_removeFromEditingBy(user), + rejectVisible: false, + acceptIcon: 'no-icon', + acceptLabel: this.translations.shared_ok(), + }); + } + }, + { allowSignalWrites: true } + ); + errorToastEffect(this.translations.events_error_changeGroups, this._actionState); const actions$ = inject(Actions); @@ -114,7 +169,41 @@ export class EventInstancesDialogComponent { .subscribe(() => this.visible.set(false)); } - public open(event: Event) { + public async open(event: Event) { + if (event.userIdEditingInstances) { + const user = this.allUsers()[event.userIdEditingInstances]; + const result = await new Promise(resolve => { + this._confirmService.confirm({ + icon: 'i-[mdi--account-lock]', + header: this.translations.events_userIsEditingAlreadyTitle(), + message: this.translations.events_userIsEditingAlready(user), + acceptLabel: this.translations.events_removeUserFromEditing(user), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translations.shared_cancel(), + rejectButtonStyleClass: 'p-button-text', + accept: () => resolve(true), + reject: () => resolve(false), + }); + }); + if (!result) return; + } + + this._store.dispatch(setEditingEventInstancesAction({ eventId: event.id, isEditing: true })); + const result = await firstValueFrom( + this._store + .select(selectEventsActionState('setInstancesEditing')) + .pipe(filter(x => isActionIdle(x))) + ); + if (hasActionFailed(result)) { + this._messageService.add({ + severity: 'error', + summary: this.translations.events_error_setEditingInstances(), + detail: this.translations.shared_tryAgainLater(), + life: 7500, + }); + return; + } + this.event.set(event); this.instances.set( event.timeslots.map(timeslot => ({ timeslot, instances: timeslot.instances })) diff --git a/src/client/src/app/i18n/de.json b/src/client/src/app/i18n/de.json index cddb82f..fc8d5fc 100644 --- a/src/client/src/app/i18n/de.json +++ b/src/client/src/app/i18n/de.json @@ -86,6 +86,10 @@ "staged": "vorbereitet", "facebookLink": "Facebook link", "visitFacebookLink": "Öffne Facebook Event", + "userIsEditingAlreadyTitle": "Bearbeitungsmodus belegt", + "userIsEditingAlready": "{{alias}} bearbeitet bereits die Gruppen der Veranstaltung.
Die Gruppen können nur von einer Person gleichzeitig bearbeitet werden.

Bitte sprich dich mit {{alias}} ab. Im Zweifel kannst du {{alias}} auch aus dem Bearbeitungsmodus entfernen.

Wichtig: Bitte beachte, dass dabei alle Änderungen von {{alias}} verloren gehen.", + "removeUserFromEditing": "{{alias}} aus dem Bearbeitungsmodus entfernen", + "removeFromEditingBy": "Du wurdest von {{alias}} aus dem Bearbeitungsmodus entfernt.", "timeslot": { "notFound": "Dieser Zeitslot existiert nicht.", "preconfiguredGroups": "Vordefinierte Gruppen", @@ -156,7 +160,8 @@ "deletePreconfig": "Fehler beim Löschen der vordefinierten Gruppe.", "start": "Fehler beim Starten der Veranstaltung.", "buildGroups": "Fehler beim Bilden der Gruppen.", - "changeGroups": "Fehler beim Ändern der Gruppen." + "changeGroups": "Fehler beim Ändern der Gruppen.", + "setEditingInstances": "Fehler beim beanspruchen der Bearbeitung der Gruppen." } }, "maps": { diff --git a/src/client/src/app/i18n/en.json b/src/client/src/app/i18n/en.json index 4ef7e9d..5f58ed0 100644 --- a/src/client/src/app/i18n/en.json +++ b/src/client/src/app/i18n/en.json @@ -86,6 +86,10 @@ "staged": "staged", "facebookLink": "Facebook link", "visitFacebookLink": "View Facebook Event", + "userIsEditingAlreadyTitle": "User is editing already", + "userIsEditingAlready": "{{alias}} is already editing the groups for the event.
The groups can only be edited by one person at a time.

Please consult with {{alias}}. If in doubt, you can also remove {{alias}} from the edit mode.

Important: Please note that all changes made by {{alias}} will be lost.", + "removeUserFromEditing": "Remove {{alias}} from edit mode", + "removeFromEditingBy": "You have been removed from edit mode by {{alias}}.", "timeslot": { "notFound": "This timeslot does not exist.", "preconfiguredGroups": "Preconfigured groups", @@ -156,7 +160,8 @@ "deletePreconfig": "Failed to delete preconfigured group.", "start": "Failed to start event.", "buildGroups": "Failed to build groups.", - "changeGroups": "Failed to change groups." + "changeGroups": "Failed to change groups.", + "setEditingInstances": "Error when claiming the editing of groups." } }, "maps": { diff --git a/src/client/src/app/models/realtime-events.ts b/src/client/src/app/models/realtime-events.ts index 52c0dd9..2793586 100644 --- a/src/client/src/app/models/realtime-events.ts +++ b/src/client/src/app/models/realtime-events.ts @@ -20,6 +20,10 @@ export type EventTimeslotChangedRealtimeEvent = { export type EventInstancesChangedRealtimeEvent = { eventId: string; }; +export type EventInstancesEditorChangedEvent = { + eventId: string; + userId: string | null | undefined; +}; export type EventPreconfigurationChangedRealtimeEvent = { eventId: string; eventTimeslotId: string; diff --git a/src/client/src/app/services/realtime-events.service.ts b/src/client/src/app/services/realtime-events.service.ts index affc9b5..f8ef9f3 100644 --- a/src/client/src/app/services/realtime-events.service.ts +++ b/src/client/src/app/services/realtime-events.service.ts @@ -31,6 +31,7 @@ import { PlayerEventRegistrationChangedRealtimeEvent, UserSettingsChangedRealtimeEvent, PlayerEventTimeslotRegistrationChanged, + EventInstancesEditorChangedEvent, } from '../models/realtime-events'; import { SignalrRetryPolicy } from '../signalr-retry-policy'; import { onDocumentVisibilityChange$ } from '../utils/event.utils'; @@ -41,6 +42,7 @@ const MAP_CHANGED = 'mapChanged'; const EVENT_CHANGED = 'eventChanged'; const EVENT_TIMESLOT_CHANGED = 'eventTimeslotChanged'; const EVENT_INSTANCES_CHANGED = 'eventInstancesChanged'; +const EVENT_INSTANCES_EDITOR_CHANGED = 'eventInstancesEditorChanged'; const EVENT_PRECONFIGURATION_CHANGED = 'eventPreconfigurationChanged'; const PLAYER_EVENT_CHANGED = 'playerEventChanged'; const PLAYER_EVENT_REGISTRATION_CHANGED = 'playerEventRegistrationChanged'; @@ -58,6 +60,8 @@ export class RealtimeEventsService implements OnDestroy { public readonly eventChanged = new EventEmitter(); public readonly eventTimeslotChanged = new EventEmitter(); public readonly eventInstancesChanged = new EventEmitter(); + public readonly eventInstancesEditorChanged = + new EventEmitter(); public readonly eventPreconfigurationChanged = new EventEmitter(); public readonly playerEventChanged = new EventEmitter(); @@ -141,6 +145,7 @@ export class RealtimeEventsService implements OnDestroy { this.on(connection, EVENT_CHANGED, this.eventChanged); this.on(connection, EVENT_TIMESLOT_CHANGED, this.eventTimeslotChanged); this.on(connection, EVENT_INSTANCES_CHANGED, this.eventInstancesChanged); + this.on(connection, EVENT_INSTANCES_EDITOR_CHANGED, this.eventInstancesEditorChanged); this.on(connection, EVENT_PRECONFIGURATION_CHANGED, this.eventPreconfigurationChanged); this.on(connection, PLAYER_EVENT_CHANGED, this.playerEventChanged); this.on(connection, PLAYER_EVENT_REGISTRATION_CHANGED, this.playerEventRegistrationChanged); @@ -162,7 +167,7 @@ export class RealtimeEventsService implements OnDestroy { methodName: message.target, arguments: message.arguments, }); - oldHandler.call(connection, message); + return oldHandler.call(connection, message); }; } else { Logger.logWarn('RealtimeEventsService', 'Cannot hook into HubConnection._invokeClientMethod'); diff --git a/src/server/data/Entities/EventEntity.cs b/src/server/data/Entities/EventEntity.cs index dbbf6dc..35d77b3 100644 --- a/src/server/data/Entities/EventEntity.cs +++ b/src/server/data/Entities/EventEntity.cs @@ -11,11 +11,11 @@ public class EventEntity public required DateTimeOffset RegistrationDeadline { get; set; } public DateTimeOffset? StartedAt { get; set; } public required bool Staged { get; set; } + public string? ExternalUri { get; set; } + public long? UserIdEditingInstances { get; set; } public List Timeslots { get; set; } = []; - public string? ExternalUri { get; set; } = null; - public static void Configure(EntityTypeBuilder builder) { builder.ToTable("events"); @@ -27,9 +27,17 @@ public static void Configure(EntityTypeBuilder builder) .HasColumnName("registration_deadline") .IsRequired(); builder.Property(x => x.StartedAt).HasColumnName("started_at"); - - builder.HasKey(x => x.Id); builder.Property(x => x.Staged).HasColumnName("staged").IsRequired(); builder.Property(x => x.ExternalUri).HasColumnName("external_uri"); + builder.Property(x => x.UserIdEditingInstances).HasColumnName("user_id_editing_instances"); + + builder.HasKey(x => x.Id); + + builder + .HasOne() + .WithMany() + .HasPrincipalKey(x => x.Id) + .HasForeignKey(x => x.UserIdEditingInstances) + .OnDelete(DeleteBehavior.SetNull); } } diff --git a/src/server/domain/Models/Event.cs b/src/server/domain/Models/Event.cs index ec175ea..afcde56 100644 --- a/src/server/domain/Models/Event.cs +++ b/src/server/domain/Models/Event.cs @@ -17,7 +17,8 @@ public record Event( [property: Required] EventTimeslot[] Timeslots, DateTimeOffset? StartedAt, bool? Staged, - string? ExternalUri + string? ExternalUri, + string? UserIdEditingInstances ); /// diff --git a/src/server/domain/Models/RealtimeEvents/RealtimeEvent.cs b/src/server/domain/Models/RealtimeEvents/RealtimeEvent.cs index f1eeb66..e39b955 100644 --- a/src/server/domain/Models/RealtimeEvents/RealtimeEvent.cs +++ b/src/server/domain/Models/RealtimeEvents/RealtimeEvent.cs @@ -70,6 +70,15 @@ public record EventInstancesChanged(string EventId) : IGroupRealtimeEvent public RealtimeEventGroup Group => RealtimeEventGroup.Admin; } + /// Event that is triggered when the editor of an event instance changed. + public record EventInstancesEditorChanged(string EventId, string? UserId) : IGroupRealtimeEvent + { + public static string MethodName => "EventInstancesEditorChanged"; + + [JsonIgnore] + public RealtimeEventGroup Group => RealtimeEventGroup.Admin; + } + /// Event that is triggered when a preconfiguration of an event timeslot changed. public record EventPreconfigurationChanged( string EventId, diff --git a/src/server/host/Endpoints/Administration/Events/Instances/SetEventInstancesEditingEndpoint.cs b/src/server/host/Endpoints/Administration/Events/Instances/SetEventInstancesEditingEndpoint.cs new file mode 100644 index 0000000..d898cbd --- /dev/null +++ b/src/server/host/Endpoints/Administration/Events/Instances/SetEventInstancesEditingEndpoint.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using FastEndpoints; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using MinigolfFriday.Data; +using MinigolfFriday.Domain.Models.RealtimeEvents; +using MinigolfFriday.Host.Common; +using MinigolfFriday.Host.Services; + +namespace MinigolfFriday.Host.Endpoints.Administration.Events.Instances; + +/// The id of the event to mark for being edited. +/// Whether the user intents to edit the event instances. +public record SetEventInstancesEditingRequest( + [property: Required] string EventId, + [property: Required] bool IsEditing +); + +public class SetEventInstancesEditingRequestValidator : Validator +{ + public SetEventInstancesEditingRequestValidator(IIdService idService) + { + RuleFor(x => x.EventId).NotEmpty().ValidSqid(idService.Event); + } +} + +/// Marks an event for being edited by the current user. +public class SetEventInstancesEditingEndpoint( + DatabaseContext databaseContext, + IIdService idService, + IJwtService jwtService, + IRealtimeEventsService realtimeEventsService +) : Endpoint +{ + public override void Configure() + { + Put("{eventId}/instances:editing"); + Group(); + this.ProducesErrors(EndpointErrors.EventNotFound, EndpointErrors.UserIdNotInClaims); + } + + public override async Task HandleAsync( + SetEventInstancesEditingRequest req, + CancellationToken ct + ) + { + if (!jwtService.TryGetUserId(User, out var userId)) + { + Logger.LogWarning(EndpointErrors.UserIdNotInClaims); + await this.SendErrorAsync(EndpointErrors.UserIdNotInClaims, ct); + return; + } + + var eventId = idService.Event.DecodeSingle(req.EventId); + var eventQuery = databaseContext.Events.Where(x => x.Id == eventId); + var eventInfo = await eventQuery + .Select(x => new { x.UserIdEditingInstances }) + .FirstOrDefaultAsync(ct); + + if (eventInfo == null) + { + Logger.LogWarning(EndpointErrors.EventNotFound, eventId); + await this.SendErrorAsync(EndpointErrors.EventNotFound, req.EventId, ct); + return; + } + + if (req.IsEditing || eventInfo.UserIdEditingInstances == userId) + { + await eventQuery.ExecuteUpdateAsync( + x => x.SetProperty(x => x.UserIdEditingInstances, req.IsEditing ? userId : null), + ct + ); + await realtimeEventsService.SendEventAsync( + new RealtimeEvent.EventInstancesEditorChanged( + idService.Event.Encode(eventId), + req.IsEditing ? idService.User.Encode(userId) : null + ), + ct + ); + } + + await SendOkAsync(ct); + } +} diff --git a/src/server/host/Mappers/EventMapper.cs b/src/server/host/Mappers/EventMapper.cs index c81f8eb..9524f18 100644 --- a/src/server/host/Mappers/EventMapper.cs +++ b/src/server/host/Mappers/EventMapper.cs @@ -17,7 +17,10 @@ public Event Map(EventEntity entity) entity.Timeslots?.Select(Map).ToArray() ?? [], entity.StartedAt, entity.Staged, - entity.ExternalUri + entity.ExternalUri, + entity.UserIdEditingInstances != null + ? idService.User.Encode(entity.UserIdEditingInstances.Value) + : null ); } diff --git a/src/server/migrations/mssql/Migrations/20240727123310_EventInstanceEditorLock.Designer.cs b/src/server/migrations/mssql/Migrations/20240727123310_EventInstanceEditorLock.Designer.cs new file mode 100644 index 0000000..3203adf --- /dev/null +++ b/src/server/migrations/mssql/Migrations/20240727123310_EventInstanceEditorLock.Designer.cs @@ -0,0 +1,634 @@ +// +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("20240727123310_EventInstanceEditorLock")] + partial class EventInstanceEditorLock + { + /// + 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("ExternalUri") + .HasColumnType("nvarchar(max)") + .HasColumnName("external_uri"); + + b.Property("RegistrationDeadline") + .HasColumnType("datetimeoffset") + .HasColumnName("registration_deadline"); + + b.Property("Staged") + .HasColumnType("bit") + .HasColumnName("staged"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("started_at"); + + b.Property("UserIdEditingInstances") + .HasColumnType("bigint") + .HasColumnName("user_id_editing_instances"); + + b.HasKey("Id"); + + b.HasIndex("UserIdEditingInstances"); + + 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.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + + 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/20240727123310_EventInstanceEditorLock.cs b/src/server/migrations/mssql/Migrations/20240727123310_EventInstanceEditorLock.cs new file mode 100644 index 0000000..362ac2a --- /dev/null +++ b/src/server/migrations/mssql/Migrations/20240727123310_EventInstanceEditorLock.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.MsSql.Migrations +{ + /// + public partial class EventInstanceEditorLock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "user_id_editing_instances", + table: "events", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_events_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances"); + + migrationBuilder.AddForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropIndex( + name: "IX_events_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropColumn( + name: "user_id_editing_instances", + table: "events"); + } + } +} diff --git a/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs index 89f46d3..c1fe76b 100644 --- a/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/mssql/Migrations/DatabaseContextModelSnapshot.cs @@ -51,8 +51,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset") .HasColumnName("started_at"); + b.Property("UserIdEditingInstances") + .HasColumnType("bigint") + .HasColumnName("user_id_editing_instances"); + b.HasKey("Id"); + b.HasIndex("UserIdEditingInstances"); + b.ToTable("events", (string)null); }); @@ -423,6 +429,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users_to_roles"); }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => { b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") diff --git a/src/server/migrations/postgresql/Migrations/20240727123312_EventInstanceEditorLock.Designer.cs b/src/server/migrations/postgresql/Migrations/20240727123312_EventInstanceEditorLock.Designer.cs new file mode 100644 index 0000000..97e0329 --- /dev/null +++ b/src/server/migrations/postgresql/Migrations/20240727123312_EventInstanceEditorLock.Designer.cs @@ -0,0 +1,632 @@ +// +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("20240727123312_EventInstanceEditorLock")] + partial class EventInstanceEditorLock + { + /// + 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("ExternalUri") + .HasColumnType("text") + .HasColumnName("external_uri"); + + 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.Property("UserIdEditingInstances") + .HasColumnType("bigint") + .HasColumnName("user_id_editing_instances"); + + b.HasKey("Id"); + + b.HasIndex("UserIdEditingInstances"); + + 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.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + + 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/20240727123312_EventInstanceEditorLock.cs b/src/server/migrations/postgresql/Migrations/20240727123312_EventInstanceEditorLock.cs new file mode 100644 index 0000000..7e0e23f --- /dev/null +++ b/src/server/migrations/postgresql/Migrations/20240727123312_EventInstanceEditorLock.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.PostgreSql.Migrations +{ + /// + public partial class EventInstanceEditorLock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "user_id_editing_instances", + table: "events", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_events_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances"); + + migrationBuilder.AddForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropIndex( + name: "IX_events_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropColumn( + name: "user_id_editing_instances", + table: "events"); + } + } +} diff --git a/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs index 872d1fe..2c67614 100644 --- a/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/postgresql/Migrations/DatabaseContextModelSnapshot.cs @@ -51,8 +51,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("started_at"); + b.Property("UserIdEditingInstances") + .HasColumnType("bigint") + .HasColumnName("user_id_editing_instances"); + b.HasKey("Id"); + b.HasIndex("UserIdEditingInstances"); + b.ToTable("events", (string)null); }); @@ -421,6 +427,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users_to_roles"); }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => { b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot") diff --git a/src/server/migrations/sqlite/Migrations/20240727123308_EventInstanceEditorLock.Designer.cs b/src/server/migrations/sqlite/Migrations/20240727123308_EventInstanceEditorLock.Designer.cs new file mode 100644 index 0000000..32f9fe0 --- /dev/null +++ b/src/server/migrations/sqlite/Migrations/20240727123308_EventInstanceEditorLock.Designer.cs @@ -0,0 +1,609 @@ +// +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("20240727123308_EventInstanceEditorLock")] + partial class EventInstanceEditorLock + { + /// + 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("ExternalUri") + .HasColumnType("TEXT") + .HasColumnName("external_uri"); + + b.Property("RegistrationDeadline") + .HasColumnType("TEXT") + .HasColumnName("registration_deadline"); + + b.Property("Staged") + .HasColumnType("INTEGER") + .HasColumnName("staged"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("UserIdEditingInstances") + .HasColumnType("INTEGER") + .HasColumnName("user_id_editing_instances"); + + b.HasKey("Id"); + + b.HasIndex("UserIdEditingInstances"); + + 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.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + + 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/20240727123308_EventInstanceEditorLock.cs b/src/server/migrations/sqlite/Migrations/20240727123308_EventInstanceEditorLock.cs new file mode 100644 index 0000000..4260e7f --- /dev/null +++ b/src/server/migrations/sqlite/Migrations/20240727123308_EventInstanceEditorLock.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MinigolfFriday.Migrations.Sqlite.Migrations +{ + /// + public partial class EventInstanceEditorLock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "user_id_editing_instances", + table: "events", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_events_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances"); + + migrationBuilder.AddForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events", + column: "user_id_editing_instances", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_events_users_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropIndex( + name: "IX_events_user_id_editing_instances", + table: "events"); + + migrationBuilder.DropColumn( + name: "user_id_editing_instances", + table: "events"); + } + } +} diff --git a/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs b/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs index ad159ee..31ffd53 100644 --- a/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/server/migrations/sqlite/Migrations/DatabaseContextModelSnapshot.cs @@ -44,8 +44,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("started_at"); + b.Property("UserIdEditingInstances") + .HasColumnType("INTEGER") + .HasColumnName("user_id_editing_instances"); + b.HasKey("Id"); + b.HasIndex("UserIdEditingInstances"); + b.ToTable("events", (string)null); }); @@ -398,6 +404,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users_to_roles"); }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventEntity", b => + { + b.HasOne("MinigolfFriday.Data.Entities.UserEntity", null) + .WithMany() + .HasForeignKey("UserIdEditingInstances") + .OnDelete(DeleteBehavior.SetNull); + }); + modelBuilder.Entity("MinigolfFriday.Data.Entities.EventInstanceEntity", b => { b.HasOne("MinigolfFriday.Data.Entities.EventTimeslotEntity", "EventTimeslot")