diff --git a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs index 87819eb..e7ca08d 100644 --- a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs +++ b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs @@ -7,34 +7,9 @@ using System.Threading.Tasks; namespace Koogle.Infrastructure.Security; - -public sealed class ClubRoleRequirement : IAuthorizationRequirement +public static class ClubRoleHelper { - public ClubRoleRequirement(string requiredRole) => RequiredRole = requiredRole; - public string RequiredRole { get; } -} - -/// -/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]). -/// Uses current_club_id claim to determine the club context. -/// -public sealed class ClubRoleHandler : AuthorizationHandler -{ - protected override Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ClubRoleRequirement requirement) - { - // Get current club from claims - var currentClubClaim = context.User.FindFirst("current_club_id"); - if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId)) - { - return Task.CompletedTask; // No club context, fail - } - - return CheckClubRole(context, requirement, clubId); - } - - private static Task CheckClubRole( + public static Task CheckClubRole( AuthorizationHandlerContext context, ClubRoleRequirement requirement, Guid clubId) @@ -72,6 +47,35 @@ public sealed class ClubRoleHandler : AuthorizationHandler } } +public sealed class ClubRoleRequirement : IAuthorizationRequirement +{ + public ClubRoleRequirement(string requiredRole) => RequiredRole = requiredRole; + public string RequiredRole { get; } +} + +/// +/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]). +/// Uses current_club_id claim to determine the club context. +/// +public sealed class ClubRoleHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ClubRoleRequirement requirement) + { + // Get current club from claims + var currentClubClaim = context.User.FindFirst("current_club_id"); + if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId)) + { + return Task.CompletedTask; // No club context, fail + } + + return ClubRoleHelper.CheckClubRole(context, requirement, clubId); + } + + +} + /// /// Handles ClubRoleRequirement when a specific clubId resource is passed. /// @@ -82,33 +86,6 @@ public sealed class ClubRoleResourceHandler : AuthorizationHandler role switch - { - "Admin" => 3, - "Editor" => 2, - "Viewer" => 1, - _ => 0 - }; - - var requiredRank = Rank(requirement.RequiredRole); - - var claims = context.User.FindAll("club_role"); - foreach (var c in claims) - { - var parts = c.Value.Split(':', 2); - if (parts.Length != 2) continue; - - if (Guid.TryParse(parts[0], out var claimClubId) && claimClubId == clubId) - { - var userRole = parts[1]; - if (Rank(userRole) >= requiredRank) - { - context.Succeed(requirement); - break; - } - } - } - - return Task.CompletedTask; + return ClubRoleHelper.CheckClubRole(context, requirement, clubId); } } diff --git a/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor b/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor new file mode 100644 index 0000000..7667986 --- /dev/null +++ b/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor @@ -0,0 +1,136 @@ +@using Koogle.Application.DTOs +@using Koogle.Domain.Enums + + + + + @(IsEditMode ? "Kosten-Vorlage bearbeiten" : "Neue Kosten-Vorlage") + + + + + + + + + + + + Geldstrafe + Sachleistung + + + + Optionen + + + + Als Schnellauswahl-Button anzeigen + + + + + Preis kann bei Zuweisung angepasst werden + + + + + Alle außer der Verursacher zahlen + + + + + Abbrechen + + @(IsEditMode ? "Speichern" : "Erstellen") + + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + public ExpenseDto? Expense { get; set; } + + private MudForm? _form; + private bool _isValid; + private string _name = ""; + private string _description = ""; + private decimal _price; + private ExpenseType _expenseType = ExpenseType.Monetary; + private bool _isOneClick; + private bool _isInverse; + private bool _isVariable; + + private bool IsEditMode => Expense is not null; + + protected override void OnInitialized() + { + if (Expense is not null) + { + _name = Expense.Name; + _description = Expense.Description; + _price = Expense.Price; + _expenseType = Expense.ExpenseType; + _isOneClick = Expense.IsOneClick; + _isInverse = Expense.IsInverse; + _isVariable = Expense.IsVariable; + } + } + + private void Cancel() => MudDialog?.Cancel(); + + private void Submit() + { + if (!_isValid) return; + + if (IsEditMode) + { + var dto = new UpdateExpenseDto + { + Id = Expense!.Id, + Name = _name, + Description = _description, + Price = _price, + ExpenseType = _expenseType, + IsOneClick = _isOneClick, + IsInverse = _isInverse, + IsVariable = _isVariable + }; + MudDialog?.Close(DialogResult.Ok(dto)); + } + else + { + var dto = new CreateExpenseDto + { + Name = _name, + Description = _description, + Price = _price, + ExpenseType = _expenseType, + IsOneClick = _isOneClick, + IsInverse = _isInverse, + IsVariable = _isVariable + }; + MudDialog?.Close(DialogResult.Ok(dto)); + } + } +} diff --git a/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor b/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor new file mode 100644 index 0000000..ac37822 --- /dev/null +++ b/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor @@ -0,0 +1,193 @@ +@page "/expenses" +@attribute [Authorize(Policy = "ClubViewer")] + +@inherits Fluxor.Blazor.Web.Components.FluxorComponent + +@using Fluxor +@using Koogle.Application.DTOs +@using Koogle.Domain.Enums +@using Koogle.Web.Store.ExpenseState +@using Microsoft.AspNetCore.Authorization + +@inject IState ExpenseState +@inject IDispatcher Dispatcher +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +Kosten-Vorlagen + +Kosten-Vorlagen + +@if (ExpenseState.Value.Error is not null) +{ + + @ExpenseState.Value.Error + +} + + + + + + + Geldstrafe + Sachleistung + + + Neue Vorlage + + + + Name + Preis + Typ + Optionen + Aktionen + + + @context.Name + @context.Price.ToString("C") + + + @GetTypeLabel(context.ExpenseType) + + + + @if (context.IsOneClick) + { + + + + } + @if (context.IsInverse) + { + + + + } + @if (context.IsVariable) + { + + + + } + + + + + + + + + + + + + + + Keine Kosten-Vorlagen gefunden + + + +@code { + private string _searchString = ""; + private ExpenseType? _typeFilter; + + private IEnumerable FilteredExpenses => _typeFilter.HasValue + ? ExpenseState.Value.Expenses.Where(e => e.ExpenseType == _typeFilter.Value) + : ExpenseState.Value.Expenses; + + protected override void OnInitialized() + { + base.OnInitialized(); + Dispatcher.Dispatch(new LoadExpensesAction()); + } + + private bool FilterFunc(ExpenseDto expense) + { + if (string.IsNullOrWhiteSpace(_searchString)) + return true; + + return expense.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase) || + expense.Description.Contains(_searchString, StringComparison.OrdinalIgnoreCase); + } + + private void ClearError() + { + Dispatcher.Dispatch(new ClearExpenseErrorAction()); + } + + private static string GetTypeLabel(ExpenseType type) => type switch + { + ExpenseType.Monetary => "Geldstrafe", + ExpenseType.Material => "Sachleistung", + _ => type.ToString() + }; + + private static Color GetTypeColor(ExpenseType type) => type switch + { + ExpenseType.Monetary => Color.Warning, + ExpenseType.Material => Color.Info, + _ => Color.Default + }; + + private async Task OpenCreateDialog() + { + var dialog = await DialogService.ShowAsync("Neue Kosten-Vorlage"); + var result = await dialog.Result; + + if (result != null && !result.Canceled && result.Data is CreateExpenseDto dto) + { + Dispatcher.Dispatch(new CreateExpenseAction(dto)); + Snackbar.Add("Vorlage wird erstellt...", Severity.Info); + } + } + + private async Task OpenEditDialog(ExpenseDto expense) + { + var parameters = new DialogParameters + { + { "Expense", expense } + }; + + var dialog = await DialogService.ShowAsync("Kosten-Vorlage bearbeiten", parameters); + var result = await dialog.Result; + + if (result != null && !result.Canceled && result.Data is UpdateExpenseDto dto) + { + Dispatcher.Dispatch(new UpdateExpenseAction(dto)); + Snackbar.Add("Vorlage wird aktualisiert...", Severity.Info); + } + } + + private async Task ConfirmDelete(ExpenseDto expense) + { + var parameters = new DialogParameters + { + { "ContentText", $"Möchten Sie \"{expense.Name}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." }, + { "ButtonText", "Löschen" }, + { "Color", Color.Error } + }; + + var dialog = await DialogService.ShowAsync("Kosten-Vorlage löschen", parameters); + var result = await dialog.Result; + + if (result != null && !result.Canceled) + { + Dispatcher.Dispatch(new DeleteExpenseAction(expense.Id)); + Snackbar.Add("Vorlage wird gelöscht...", Severity.Info); + } + } +}