add demo-club:

Neue Dateien (5)

  | Datei                                                  | Zweck                |
  |--------------------------------------------------------|----------------------|
  | src/Koogle.Infrastructure/Data/DemoSeeder.cs           | Seeder + Reset-Logik |
  | src/Koogle.Domain/Interfaces/IDemoResetService.cs      | Interface            |
  | src/Koogle.Infrastructure/Services/DemoResetService.cs | Service              |
  | src/Koogle.Web/Store/Demo/DemoActions.cs               | Fluxor Actions       |
  | src/Koogle.Web/Store/Demo/DemoEffects.cs               | Fluxor Effects       |

  Geaenderte Dateien (6)

  | Datei                        | Aenderung                     |
  |------------------------------|-------------------------------|
  | appsettings.Development.json | Demo-Config hinzugefuegt      |
  | Program.cs                   | DemoSeeder.SeedAsync() Aufruf |
  | DependencyInjection.cs       | IDemoResetService registriert |
  | Login.razor                  | Demo-Hinweis-Box              |
  | Clubs.razor                  | Reset-Button + Confirm-Dialog |

  Demo-Daten

  - User: demo@koogle.de / demo123 (ClubAdmin, kein SuperAdmin)
  - Club: "Demo"
  - 8 Mitglieder: Hans Maier, Klaus Schmidt, Werner Braun, Dieter Fischer, Juergen Weber, Heinz Mueller, Rolf Schneider, Karl Hoffmann
  - 2 Gaeste: Stefan Gast, Thomas Besucher
  - 10 Expenses mit Trigger-Zuordnung (Gosse 0.50, Pudel 0.30, ... Abwesenheit 5.00)
  - 3 Spieltage mit Games
This commit is contained in:
beo3000 2025-12-28 12:10:15 +01:00
parent fcfbbae94e
commit 5c088345b3
10 changed files with 617 additions and 1 deletions

View File

@ -0,0 +1,23 @@
namespace Koogle.Domain.Interfaces;
/// <summary>
/// Service for resetting the Demo club to initial state.
/// </summary>
public interface IDemoResetService
{
/// <summary>
/// Resets the Demo club to initial state (hard delete + re-seed).
/// </summary>
/// <returns>True if reset was successful, false if Demo club not found or not enabled.</returns>
Task<bool> ResetDemoClubAsync(CancellationToken ct = default);
/// <summary>
/// Checks if the given club ID is the Demo club.
/// </summary>
bool IsDemoClub(Guid clubId);
/// <summary>
/// Gets the Demo club ID if Demo mode is enabled.
/// </summary>
Guid? GetDemoClubId();
}

View File

@ -0,0 +1,407 @@
using Koogle.Domain.Entities;
using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
using Koogle.Infrastructure.Identity;
using KoogleApp.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Koogle.Infrastructure.Data;
/// <summary>
/// Seeds demo data for the Demo club including user, persons, expenses, and sample days.
/// </summary>
public static class DemoSeeder
{
private static readonly string[] MemberNames =
{
"Hans Maier", "Klaus Schmidt", "Werner Braun", "Dieter Fischer",
"Juergen Weber", "Heinz Mueller", "Rolf Schneider", "Karl Hoffmann"
};
private static readonly string[] GuestNames = { "Stefan Gast", "Thomas Besucher" };
/// <summary>
/// Seeds demo user, club membership, persons, expenses, triggers and sample days.
/// </summary>
public static async Task SeedAsync(IServiceProvider services, IConfiguration config, IHostEnvironment env)
{
var demoEnabled = config.GetValue<bool>("Bootstrap:Demo:Enabled");
if (!demoEnabled) return;
var allowSeed = env.IsDevelopment() || config.GetValue<bool>("Bootstrap:EnableSeeding");
if (!allowSeed) return;
using var scope = services.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
var demoEmail = config["Bootstrap:Demo:Email"] ?? "demo@koogle.de";
var demoPassword = config["Bootstrap:Demo:Password"] ?? "demo123";
var demoClubName = config["Bootstrap:Demo:ClubName"] ?? "Demo";
// 1) Find or create Demo Club (should exist from BootstrapSeeder if DefaultClub.Name == "Demo")
var club = await appDb.Clubs.FirstOrDefaultAsync(c => c.Name == demoClubName && !c.IsDeleted);
if (club == null)
{
club = new Club { Name = demoClubName, CreatedAt = DateTime.UtcNow };
appDb.Clubs.Add(club);
await appDb.SaveChangesAsync();
}
// 2) Create Demo User (NOT SuperAdmin)
var demoUser = await userManager.FindByEmailAsync(demoEmail);
if (demoUser == null)
{
demoUser = new ApplicationUser
{
UserName = demoEmail,
Email = demoEmail,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(demoUser, demoPassword);
if (!result.Succeeded)
{
throw new InvalidOperationException("Failed to create Demo user: " +
string.Join("; ", result.Errors.Select(e => $"{e.Code}:{e.Description}")));
}
}
// 3) Create UserProfile for Demo user
var profile = await appDb.UserProfiles.FirstOrDefaultAsync(p => p.IdentityUserId == demoUser.Id);
if (profile == null)
{
profile = new UserProfile
{
IdentityUserId = demoUser.Id,
DisplayName = "Demo Benutzer",
Locale = "de-DE",
TimeZone = "Europe/Berlin",
CreatedAt = DateTime.UtcNow
};
appDb.UserProfiles.Add(profile);
await appDb.SaveChangesAsync();
}
// 4) Add Demo user to Demo club
var membership = await appDb.UserProfileClubs.FindAsync(profile.Id, club.Id);
if (membership == null)
{
membership = new UserProfileClub
{
UserProfileId = profile.Id,
ClubId = club.Id,
IsDefault = true,
AssignedAt = DateTime.UtcNow,
AssignedById = demoUser.Id
};
appDb.UserProfileClubs.Add(membership);
await appDb.SaveChangesAsync();
}
// 5) Assign ClubAdmin role to Demo user (NOT SuperAdmin!)
var roleExists = await appDb.UserProfileClubRoleAssignments.AnyAsync(a =>
a.UserProfileId == profile.Id && a.ClubId == club.Id && a.RoleName == "Admin" && !a.IsDeleted);
if (!roleExists)
{
appDb.UserProfileClubRoleAssignments.Add(new UserProfileClubRoleAssignment
{
UserProfileId = profile.Id,
ClubId = club.Id,
RoleId = Guid.Empty,
RoleName = "Admin",
AssignedAt = DateTime.UtcNow,
AssignedById = demoUser.Id,
CreatedAt = DateTime.UtcNow
});
await appDb.SaveChangesAsync();
}
// 6) Seed demo data if not already present
var hasPersons = await appDb.Persons.AnyAsync(p => p.ClubId == club.Id && !p.IsDeleted);
if (!hasPersons)
{
await SeedDemoDataAsync(appDb, triggerRepo, club.Id, demoUser.Id);
}
}
/// <summary>
/// Resets the Demo club to initial state: hard delete all data, then re-seed.
/// </summary>
public static async Task ResetDemoClubAsync(IServiceProvider services, Guid clubId)
{
using var scope = services.CreateScope();
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
// Find creator ID for seeding
var club = await appDb.Clubs.FindAsync(clubId);
if (club == null) return;
var membership = await appDb.UserProfileClubs.FirstOrDefaultAsync(m => m.ClubId == clubId);
var creatorId = membership?.AssignedById ?? Guid.Empty;
// Hard delete all club data
await HardDeleteClubDataAsync(appDb, clubId);
// Re-seed demo data
await SeedDemoDataAsync(appDb, triggerRepo, clubId, creatorId);
}
private static async Task HardDeleteClubDataAsync(AppDbContext db, Guid clubId)
{
// Order matters due to FK constraints - delete children first
await db.Database.ExecuteSqlAsync($"DELETE FROM app.PersonExpenses WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.GamePersons WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Games WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.DayPersons WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Days WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ExpenseTriggers WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Expenses WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Persons WHERE ClubId = {clubId}");
}
private static async Task SeedDemoDataAsync(AppDbContext db, ITriggerRepository triggerRepo, Guid clubId, Guid creatorId)
{
var now = DateTime.UtcNow;
// 1) Seed Persons (8 Members + 2 Guests)
var persons = new List<Person>();
foreach (var name in MemberNames)
{
var person = new Person
{
Name = name,
PersonStatus = PersonStatus.Member,
ClubId = clubId,
CreatedAt = now,
CreatedById = creatorId
};
db.Persons.Add(person);
persons.Add(person);
}
foreach (var name in GuestNames)
{
var person = new Person
{
Name = name,
PersonStatus = PersonStatus.Guest,
ClubId = clubId,
CreatedAt = now,
CreatedById = creatorId
};
db.Persons.Add(person);
persons.Add(person);
}
await db.SaveChangesAsync();
// 2) Seed Expenses with Trigger mappings
var expenseData = new (string Name, decimal Price, ExpenseTriggerType TriggerType, bool IsInverse)[]
{
("Gosse", 0.50m, ExpenseTriggerType.Gutter, false),
("Pudel", 0.30m, ExpenseTriggerType.NoWood, false),
("Klingel", 0.20m, ExpenseTriggerType.Bell, false),
("Anwurffehler", 0.40m, ExpenseTriggerType.FirstThrowFail, false),
("Kranz", 0.50m, ExpenseTriggerType.Circle, true),
("Alle Neune", 1.00m, ExpenseTriggerType.Strike, true),
("Gosse Anwurf", 1.00m, ExpenseTriggerType.FullGutter, false),
("Ausgeschieden", 2.00m, ExpenseTriggerType.Eliminated, false),
("Abwesenheit", 5.00m, ExpenseTriggerType.Absent, false),
("Strafpunkt", 0.10m, ExpenseTriggerType.ExpensePoint, false)
};
var expenses = new List<Expense>();
foreach (var (name, price, triggerType, isInverse) in expenseData)
{
var expense = new Expense
{
ClubId = clubId,
Name = name,
Description = $"Strafe fuer {name}",
Price = price,
IsOneClick = true,
IsInverse = isInverse,
IsVariable = false,
ExpenseType = ExpenseType.Monetary,
CreatedAt = now,
CreatedById = creatorId
};
db.Expenses.Add(expense);
expenses.Add(expense);
}
await db.SaveChangesAsync();
// 3) Create ExpenseTrigger mappings
for (int i = 0; i < expenses.Count; i++)
{
var expense = expenses[i];
var triggerType = expenseData[i].TriggerType;
var trigger = await triggerRepo.GetByTriggerTypeAsync(triggerType);
if (trigger != null)
{
var expenseTrigger = new ExpenseTrigger
{
ClubId = clubId,
ExpenseId = expense.Id,
TriggerId = trigger.Id,
AssignedAt = now,
AssignedById = creatorId
};
db.ExpenseTriggers.Add(expenseTrigger);
}
}
await db.SaveChangesAsync();
// 4) Seed Sample Days with Games
await SeedSampleDaysAsync(db, clubId, creatorId, persons, expenses, now);
}
private static async Task SeedSampleDaysAsync(
AppDbContext db,
Guid clubId,
Guid creatorId,
List<Person> persons,
List<Expense> expenses,
DateTime now)
{
var members = persons.Where(p => p.PersonStatus == PersonStatus.Member).ToList();
var gosseExpense = expenses.First(e => e.Name == "Gosse");
var pudelExpense = expenses.First(e => e.Name == "Pudel");
var klingelExpense = expenses.First(e => e.Name == "Klingel");
// Day 1: 3 participants, 1 game, some expenses
var day1 = new Day
{
PostDate = now.AddDays(-14),
Status = DayStatus.Closed,
ClubId = clubId,
CreatedAt = now,
CreatedById = creatorId
};
db.Days.Add(day1);
await db.SaveChangesAsync();
var day1Participants = members.Take(3).ToList();
foreach (var p in day1Participants)
{
db.Set<DayPerson>().Add(new DayPerson { DayId = day1.Id, ClubId = clubId, PersonId = p.Id });
}
var game1 = new Game
{
DayId = day1.Id,
ClubId = clubId,
GameType = "Training",
Status = GameStatus.Completed,
StartedAt = day1.PostDate,
CompletedAt = day1.PostDate.AddHours(2),
CreatedAt = now,
CreatedById = creatorId
};
db.Games.Add(game1);
await db.SaveChangesAsync();
foreach (var p in day1Participants)
{
db.Set<GamePerson>().Add(new GamePerson { GameId = game1.Id, ClubId = clubId, PersonId = p.Id });
}
await db.SaveChangesAsync();
// Day 2: 5 participants, 2 games, diverse expenses
var day2 = new Day
{
PostDate = now.AddDays(-7),
Status = DayStatus.Closed,
ClubId = clubId,
CreatedAt = now,
CreatedById = creatorId
};
db.Days.Add(day2);
await db.SaveChangesAsync();
var day2Participants = members.Take(5).ToList();
foreach (var p in day2Participants)
{
db.Set<DayPerson>().Add(new DayPerson { DayId = day2.Id, ClubId = clubId, PersonId = p.Id });
}
var game2a = new Game
{
DayId = day2.Id,
ClubId = clubId,
GameType = "Training",
Status = GameStatus.Completed,
StartedAt = day2.PostDate,
CompletedAt = day2.PostDate.AddHours(1),
CreatedAt = now,
CreatedById = creatorId
};
var game2b = new Game
{
DayId = day2.Id,
ClubId = clubId,
GameType = "Totenkiste",
Status = GameStatus.Completed,
StartedAt = day2.PostDate.AddHours(1),
CompletedAt = day2.PostDate.AddHours(2),
CreatedAt = now,
CreatedById = creatorId
};
db.Games.Add(game2a);
db.Games.Add(game2b);
await db.SaveChangesAsync();
foreach (var p in day2Participants)
{
db.Set<GamePerson>().Add(new GamePerson { GameId = game2a.Id, ClubId = clubId, PersonId = p.Id });
db.Set<GamePerson>().Add(new GamePerson { GameId = game2b.Id, ClubId = clubId, PersonId = p.Id });
}
await db.SaveChangesAsync();
// Day 3: 4 participants, 1 game (recent, still open)
var day3 = new Day
{
PostDate = now.AddDays(-1),
Status = DayStatus.Started,
ClubId = clubId,
CreatedAt = now,
CreatedById = creatorId
};
db.Days.Add(day3);
await db.SaveChangesAsync();
var day3Participants = members.Take(4).ToList();
foreach (var p in day3Participants)
{
db.Set<DayPerson>().Add(new DayPerson { DayId = day3.Id, ClubId = clubId, PersonId = p.Id });
}
var game3 = new Game
{
DayId = day3.Id,
ClubId = clubId,
GameType = "Training",
Status = GameStatus.Active,
StartedAt = day3.PostDate,
CreatedAt = now,
CreatedById = creatorId
};
db.Games.Add(game3);
await db.SaveChangesAsync();
foreach (var p in day3Participants)
{
db.Set<GamePerson>().Add(new GamePerson { GameId = game3.Id, ClubId = clubId, PersonId = p.Id });
}
await db.SaveChangesAsync();
}
}

View File

@ -4,6 +4,7 @@ using Koogle.Infrastructure.Data;
using Koogle.Infrastructure.Identity;
using Koogle.Infrastructure.Repositories;
using Koogle.Infrastructure.Security;
using Koogle.Infrastructure.Services;
using KoogleApp.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@ -84,6 +85,7 @@ public static class DependencyInjection
// Services
//services.AddScoped<IEmailService, StubEmailService>();
services.AddScoped<IDemoResetService, DemoResetService>();
services.AddCascadingAuthenticationState();

View File

@ -0,0 +1,78 @@
using Koogle.Domain.Interfaces;
using Koogle.Infrastructure.Data;
using KoogleApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Koogle.Infrastructure.Services;
/// <summary>
/// Service for resetting the Demo club to initial state.
/// </summary>
public class DemoResetService : IDemoResetService
{
private readonly IServiceProvider _services;
private readonly IConfiguration _config;
private readonly AppDbContext _db;
private Guid? _demoClubId;
public DemoResetService(
IServiceProvider services,
IConfiguration config,
AppDbContext db)
{
_services = services;
_config = config;
_db = db;
}
/// <inheritdoc />
public async Task<bool> ResetDemoClubAsync(CancellationToken ct = default)
{
var clubId = await GetDemoClubIdAsync(ct);
if (!clubId.HasValue) return false;
await DemoSeeder.ResetDemoClubAsync(_services, clubId.Value);
return true;
}
/// <inheritdoc />
public bool IsDemoClub(Guid clubId)
{
var demoClubId = GetDemoClubId();
return demoClubId.HasValue && demoClubId.Value == clubId;
}
/// <inheritdoc />
public Guid? GetDemoClubId()
{
if (_demoClubId.HasValue) return _demoClubId;
var demoEnabled = _config.GetValue<bool>("Bootstrap:Demo:Enabled");
if (!demoEnabled) return null;
var demoClubName = _config["Bootstrap:Demo:ClubName"] ?? "Demo";
var club = _db.Clubs
.AsNoTracking()
.FirstOrDefault(c => c.Name == demoClubName && !c.IsDeleted);
_demoClubId = club?.Id;
return _demoClubId;
}
private async Task<Guid?> GetDemoClubIdAsync(CancellationToken ct)
{
if (_demoClubId.HasValue) return _demoClubId;
var demoEnabled = _config.GetValue<bool>("Bootstrap:Demo:Enabled");
if (!demoEnabled) return null;
var demoClubName = _config["Bootstrap:Demo:ClubName"] ?? "Demo";
var club = await _db.Clubs
.AsNoTracking()
.FirstOrDefaultAsync(c => c.Name == demoClubName && !c.IsDeleted, ct);
_demoClubId = club?.Id;
return _demoClubId;
}
}

View File

@ -6,6 +6,7 @@
@inject NavigationManager NavigationManager
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
@inject IHttpContextAccessor HttpContextAccessor
@inject IConfiguration Configuration
<form method="post" action="/auth/login">
@ -28,6 +29,12 @@
{
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
}
@if (_demoEnabled)
{
<MudAlert Severity="Severity.Info" Class="mb-3" Icon="@Icons.Material.Filled.Info">
<strong>Demo-Zugang:</strong> @_demoEmail / @_demoPassword
</MudAlert>
}
<MudText Typo="Typo.h5" Class="mb-4">
Anmeldung
@ -93,6 +100,9 @@
private string _antiToken = "";
private bool _registered;
private string? _inviteToken;
private bool _demoEnabled;
private string _demoEmail = "";
private string _demoPassword = "";
protected override void OnInitialized()
{
@ -119,6 +129,14 @@
var http = HttpContextAccessor.HttpContext!;
var tokens = Antiforgery.GetAndStoreTokens(http);
_antiToken = tokens.RequestToken!;
// Demo credentials from configuration
_demoEnabled = Configuration.GetValue<bool>("Bootstrap:Demo:Enabled");
if (_demoEnabled)
{
_demoEmail = Configuration["Bootstrap:Demo:Email"] ?? "demo@koogle.de";
_demoPassword = Configuration["Bootstrap:Demo:Password"] ?? "demo123";
}
}
}

View File

@ -5,14 +5,17 @@
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Domain.Interfaces
@using Koogle.Domain.Enums
@using Koogle.Web.Store.ClubState
@using Koogle.Web.Store.Demo
@using Microsoft.AspNetCore.Authorization
@inject IState<ClubState> ClubState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IDemoResetService DemoResetService
<PageTitle>Clubs verwalten</PageTitle>
@ -66,6 +69,15 @@
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context))"/>
</MudTooltip>
@if (DemoResetService.IsDemoClub(context.Id))
{
<MudTooltip Text="Demo zuruecksetzen">
<MudIconButton Icon="@Icons.Material.Filled.RestartAlt"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => ConfirmResetDemo(context))"/>
</MudTooltip>
}
<MudTooltip Text="Löschen">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
@ -167,4 +179,23 @@
Snackbar.Add("Club wird gelöscht...", Severity.Info);
}
}
private async Task ConfirmResetDemo(ClubDto club)
{
var parameters = new DialogParameters
{
{ "ContentText", $"Möchtest du den Demo-Club \"{club.Name}\" auf den Ausgangszustand zurücksetzen? Alle Spieltage, Spiele und Kosten werden gelöscht." },
{ "ButtonText", "Zurücksetzen" },
{ "Color", Color.Warning }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("Demo zurücksetzen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
Dispatcher.Dispatch(new ResetDemoAction());
Snackbar.Add("Demo-Club wird zurückgesetzt...", Severity.Info);
}
}
}

View File

@ -107,6 +107,7 @@ using (var scope = app.Services.CreateScope())
}
await BootstrapSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
await BootstrapSeeder.SeedTriggersAsync(app.Services);
await DemoSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
app.Run();

View File

@ -0,0 +1,16 @@
namespace Koogle.Web.Store.Demo;
/// <summary>
/// Action to trigger Demo club reset.
/// </summary>
public record ResetDemoAction;
/// <summary>
/// Action dispatched when Demo reset succeeds.
/// </summary>
public record ResetDemoSuccessAction;
/// <summary>
/// Action dispatched when Demo reset fails.
/// </summary>
public record ResetDemoFailureAction(string Error);

View File

@ -0,0 +1,35 @@
using Fluxor;
using Koogle.Domain.Interfaces;
namespace Koogle.Web.Store.Demo;
public class DemoEffects
{
private readonly IDemoResetService _demoResetService;
public DemoEffects(IDemoResetService demoResetService)
{
_demoResetService = demoResetService;
}
[EffectMethod]
public async Task HandleResetDemo(ResetDemoAction action, IDispatcher dispatcher)
{
try
{
var success = await _demoResetService.ResetDemoClubAsync();
if (success)
{
dispatcher.Dispatch(new ResetDemoSuccessAction());
}
else
{
dispatcher.Dispatch(new ResetDemoFailureAction("Demo-Club nicht gefunden oder Demo-Modus nicht aktiviert."));
}
}
catch (Exception ex)
{
dispatcher.Dispatch(new ResetDemoFailureAction(ex.Message));
}
}
}

View File

@ -1,5 +1,4 @@
{
"Bootstrap": {
"EnableSeeding": true,
"CreateDefaultClub": true,
@ -10,6 +9,12 @@
},
"DefaultClub": {
"Name": "Demo"
},
"Demo": {
"Enabled": true,
"Email": "demo@koogle.de",
"Password": "Demo123!",
"ClubName": "Demo"
}
},
"DetailedErrors": true,