Compare commits
2 Commits
db0a4b7c00
...
8a92555ce9
| Author | SHA1 | Date |
|---|---|---|
|
|
8a92555ce9 | |
|
|
b8e18a52b3 |
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Koogle.Application.DTOs
|
||||
{
|
||||
public record LoginDto
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string ClubName { get; set; } = string.Empty;
|
||||
public bool RememberMe { get; set; }
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Koogle.Infrastructure.Identity;
|
||||
|
||||
namespace Koogle.Application.DTOs
|
||||
{
|
||||
public record UserDto
|
||||
{
|
||||
public ApplicationUser ApplicationUser { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,8 @@ namespace Koogle.Application
|
|||
//services.AddScoped<IDayService, DayService>();
|
||||
//services.AddScoped<IYearService, YearService>();
|
||||
//services.AddScoped<ICompanyService, CompanyService>();
|
||||
//services.AddScoped<IUserService, UserService>();
|
||||
//services.AddScoped<ITagService, TagService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<ICurrentClubContext, CurrentClubContext>();
|
||||
//services.AddScoped<IAuthorizationService, AuthorizationService>();
|
||||
|
||||
return services;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Koogle.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Ermöglicht den Zugriff auf den aktuellen Club-Kontext des angemeldeten Benutzers.
|
||||
/// </summary>
|
||||
public interface ICurrentClubContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Id des aktuellen Clubs des angemeldeten Benutzers.
|
||||
/// </summary>
|
||||
Guid ClubId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name des aktuellen Clubs des angemeldeten Benutzers.
|
||||
/// </summary>
|
||||
string ClubName { get; }
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Koogle.Application.DTOs;
|
||||
|
||||
namespace Koogle.Application.Interfaces
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task<UserDto?> LoginAsync(LoginDto login, CancellationToken cancellationToken = default);
|
||||
Task SignOutAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using Koogle.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Koogle.Application.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CurrentClubContext(IHttpContextAccessor http) : ICurrentClubContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid ClubId
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = http.HttpContext?.User?.FindFirstValue("current_club_id");
|
||||
return Guid.TryParse(value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ClubName =>
|
||||
http.HttpContext?.User?.FindFirstValue("current_club_name") ?? string.Empty;
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Infrastructure.Identity;
|
||||
using KoogleApp.Data;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Koogle.Application.Services;
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly AppDbContext _appDb;
|
||||
private readonly IHostEnvironmentAuthenticationStateProvider? _hostAuthentication;
|
||||
|
||||
public UserService(SignInManager<ApplicationUser> signInManager,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IHostEnvironmentAuthenticationStateProvider? hostAuthentication,
|
||||
AppDbContext appDb)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_appDb = appDb;
|
||||
_hostAuthentication = hostAuthentication;
|
||||
}
|
||||
|
||||
public async Task<UserDto?> LoginAsync(LoginDto login, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
// 1) User finden
|
||||
var user = await _userManager.FindByEmailAsync(login.Email);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2) Passwort prüfen
|
||||
var result = await _signInManager.CheckPasswordSignInAsync(user, login.Password, lockoutOnFailure: false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3) Club anhand Name finden (case-insensitive)
|
||||
var clubName = login.ClubName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(clubName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var club = await _appDb.Clubs
|
||||
.IgnoreQueryFilters() // optional, falls du SoftDelete filterst und trotzdem finden willst
|
||||
.FirstOrDefaultAsync(c => c.Name.ToLower() == clubName.ToLower() && !c.IsDeleted);
|
||||
|
||||
if (club == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4) Berechtigung prüfen: SuperAdmin ODER Mitglied im Club
|
||||
var isSuperAdmin = await _userManager.IsInRoleAsync(user, "SuperAdmin");
|
||||
|
||||
if (!isSuperAdmin)
|
||||
{
|
||||
var profile = await _appDb.UserProfiles
|
||||
.FirstOrDefaultAsync(p => p.IdentityUserId == user.Id);
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mitgliedschaft prüfen (UserProfileClub)
|
||||
var isMember = await _appDb.UserProfileClubs.AnyAsync(upc =>
|
||||
upc.UserProfileId == profile.Id &&
|
||||
upc.ClubId == club.Id &&
|
||||
!upc.Club.IsDeleted &&
|
||||
!upc.UserProfile.IsDeleted);
|
||||
|
||||
if (!isMember)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Claims für aktuellen Club setzen (im Cookie!)
|
||||
var additionalClaims = new List<Claim>
|
||||
{
|
||||
new Claim("current_club_id", club.Id.ToString()),
|
||||
new Claim("current_club_name", club.Name)
|
||||
};
|
||||
|
||||
//ClaimsPrincipal principal = await _signInManager.CreateUserPrincipalAsync(user);
|
||||
//_signInManager.Context.User = principal;
|
||||
//_signInManager.SignInAsync()
|
||||
|
||||
//_hostAuthentication!.SetAuthenticationState(
|
||||
// Task.FromResult(new AuthenticationState(principal)));
|
||||
|
||||
// 6) SignIn mit zusätzlichen Claims (werden im Cookie gespeichert) [1](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signinwithclaimsasync?view=aspnetcore-9.0)
|
||||
try
|
||||
{
|
||||
await _signInManager.SignInWithClaimsAsync(user, isPersistent: login.RememberMe, additionalClaims: additionalClaims);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
// 7) UserDto zurückgeben
|
||||
return new UserDto
|
||||
{
|
||||
ApplicationUser = user
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SignOutAsync()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,12 @@ public static class DependencyInjection
|
|||
.AddDefaultTokenProviders()
|
||||
.AddClaimsPrincipalFactory<CustomClaimsPrincipalFactory>();
|
||||
|
||||
|
||||
services.AddAuthentication("Cookies")
|
||||
.AddCookie("Cookies", options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.AccessDeniedPath = "/login";
|
||||
});
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
using Koogle.Domain.Entities;
|
||||
using Koogle.Infrastructure.Identity;
|
||||
using KoogleApp.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Koogle.Infrastructure.Security;
|
||||
|
||||
|
||||
public static class BootstrapSeeder
|
||||
{
|
||||
public static async Task SeedAsync(IServiceProvider services, IConfiguration config, IHostEnvironment env)
|
||||
{
|
||||
// Nur in Dev automatisch – Prod nur mit explizitem Flag
|
||||
var allowSeed = env.IsDevelopment() || config.GetValue<bool>("Bootstrap:EnableSeeding");
|
||||
if (!allowSeed) return;
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
|
||||
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// 1) Rollen
|
||||
var roles = new[] { "SuperAdmin", "Admin", "Editor", "Viewer" };
|
||||
foreach (var r in roles)
|
||||
{
|
||||
if (!await roleManager.RoleExistsAsync(r))
|
||||
await roleManager.CreateAsync(new ApplicationRole { Name = r });
|
||||
}
|
||||
|
||||
// 2) SuperAdmin User (Identity)
|
||||
var adminEmail = config["Bootstrap:SuperAdmin:Email"];
|
||||
var adminUserName = config["Bootstrap:SuperAdmin:UserName"] ?? adminEmail;
|
||||
var adminPassword = config["Bootstrap:SuperAdmin:Password"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(adminEmail) || string.IsNullOrWhiteSpace(adminPassword))
|
||||
throw new InvalidOperationException("Bootstrap SuperAdmin credentials missing. Set Bootstrap:SuperAdmin:Email and :Password.");
|
||||
|
||||
var adminUser = await userManager.FindByEmailAsync(adminEmail);
|
||||
if (adminUser == null)
|
||||
{
|
||||
adminUser = new ApplicationUser
|
||||
{
|
||||
UserName = adminUserName,
|
||||
Email = adminEmail,
|
||||
EmailConfirmed = env.IsDevelopment() // Dev: praktisch, Prod: lieber false + Confirm Flow
|
||||
};
|
||||
|
||||
var createResult = await userManager.CreateAsync(adminUser, adminPassword);
|
||||
if (!createResult.Succeeded)
|
||||
throw new InvalidOperationException("Failed to create SuperAdmin: " +
|
||||
string.Join("; ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
|
||||
}
|
||||
|
||||
// 3) Rolle zuweisen
|
||||
if (!await userManager.IsInRoleAsync(adminUser, "SuperAdmin"))
|
||||
await userManager.AddToRoleAsync(adminUser, "SuperAdmin");
|
||||
|
||||
// 4) Domain UserProfile anlegen
|
||||
var profile = await appDb.UserProfiles.SingleOrDefaultAsync(p => p.IdentityUserId == adminUser.Id);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = new UserProfile
|
||||
{
|
||||
IdentityUserId = adminUser.Id,
|
||||
DisplayName = "Super Admin",
|
||||
Locale = "de-DE",
|
||||
TimeZone = "Europe/Berlin",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
appDb.UserProfiles.Add(profile);
|
||||
await appDb.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Optional: Default-Club erstellen + Membership + RoleAssignments (nur wenn gewünscht)
|
||||
if (config.GetValue<bool>("Bootstrap:CreateDefaultClub"))
|
||||
{
|
||||
var clubName = config["Bootstrap:DefaultClub:Name"] ?? "Default Club";
|
||||
|
||||
var club = await appDb.Clubs.FirstOrDefaultAsync(c => c.Name == clubName);
|
||||
if (club == null)
|
||||
{
|
||||
club = new Club { Name = clubName, CreatedAt = DateTime.UtcNow };
|
||||
appDb.Clubs.Add(club);
|
||||
await appDb.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Membership (UserProfileClub) – wenn du das nutzt
|
||||
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 = adminUser.Id
|
||||
};
|
||||
appDb.UserProfileClubs.Add(membership);
|
||||
await appDb.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Club-Rollen assignments (B2): Admin/Editor/Viewer o.ä.
|
||||
// Hier: Admin im Default-Club
|
||||
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, // optional, wenn du RoleId nicht zwingend brauchst
|
||||
RoleName = "Admin",
|
||||
AssignedAt = DateTime.UtcNow,
|
||||
AssignedById = adminUser.Id,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await appDb.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,25 @@
|
|||
<base href="/" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["Koogle.Web.styles.css"]" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
|
||||
<ImportMap />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<Routes @rendermode="RenderModeForPage" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
|
||||
? new InteractiveServerRenderMode(prerender: false)
|
||||
: new InteractiveServerRenderMode(prerender: true);
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Koogle.Application.Services
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
@inject IUserService UserService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ICurrentClubContext CurrentClubContext
|
||||
|
||||
<h3>AuthTest-Component</h3>
|
||||
|
||||
|
|
@ -14,7 +18,7 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
<p>Keine Berechtigung</p>
|
||||
<p>Keine Berechtigung zum Editieren</p>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -24,10 +28,14 @@ else
|
|||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<AuthorizeView>
|
||||
aktueller Club: @CurrentClubContext.ClubName
|
||||
|
||||
<LogoutButton/>
|
||||
</AuthorizeView>
|
||||
@code {
|
||||
|
||||
[Parameter] public Guid ClubId { get; set; }
|
||||
|
||||
|
||||
private bool _canEdit;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
|
|
@ -35,7 +43,7 @@ else
|
|||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var user = state.User;
|
||||
|
||||
var result = await AuthorizationService.AuthorizeAsync(user, ClubId, "ClubEditor");
|
||||
var result = await AuthorizationService.AuthorizeAsync(user, CurrentClubContext.ClubId, "ClubEditor");
|
||||
_canEdit = result.Succeeded;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
@inherits LayoutComponentBase
|
||||
|
||||
@* Required *@
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
|
||||
@* Needed for dialogs *@
|
||||
<MudDialogProvider />
|
||||
|
||||
@* Needed for snackbars *@
|
||||
<MudSnackbarProvider />
|
||||
|
||||
@Body
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
@page "/account/login"
|
||||
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
|
||||
<form method="post" action="/auth/login">
|
||||
<!-- Hidden Fields -->
|
||||
<input type="hidden" name="ReturnUrl" value="@_returnUrl" />
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@_error</MudAlert>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-4">
|
||||
Anmeldung
|
||||
</MudText>
|
||||
|
||||
<MudTextField T="string"
|
||||
Name="Email"
|
||||
Label="E-Mail"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
InputType="InputType.Email"
|
||||
AutoComplete="username"
|
||||
Class="mb-3" />
|
||||
|
||||
<MudTextField T="string"
|
||||
Name="Password"
|
||||
Label="Passwort"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
InputType="InputType.Password"
|
||||
AutoComplete="current-password"
|
||||
Class="mb-3" />
|
||||
|
||||
<MudTextField T="string"
|
||||
Name="ClubName"
|
||||
Label="Club"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
Placeholder="Clubname"
|
||||
Class="mb-3" />
|
||||
|
||||
<MudCheckBox T="bool"
|
||||
Name="RememberMe"
|
||||
Label="Angemeldet bleiben"
|
||||
Value="true"
|
||||
Class="mb-4" />
|
||||
|
||||
<MudButton
|
||||
ButtonType="ButtonType.Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true">
|
||||
Anmelden
|
||||
</MudButton>
|
||||
|
||||
</MudPaper>
|
||||
</form>
|
||||
|
||||
|
||||
@code {
|
||||
private string _returnUrl = "/";
|
||||
private string? _error = string.Empty;
|
||||
private string _antiToken = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
|
||||
|
||||
if (query.TryGetValue("returnUrl", out var ru)) _returnUrl = ru!;
|
||||
if (query.TryGetValue("error", out var err))
|
||||
{
|
||||
_error = "Login fehlgeschlagen";
|
||||
}
|
||||
|
||||
|
||||
// Antiforgery Token generieren (klassischer MVC Token)
|
||||
var http = HttpContextAccessor.HttpContext!;
|
||||
var tokens = Antiforgery.GetAndStoreTokens(http);
|
||||
_antiToken = tokens.RequestToken!;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
@page "/"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Koogle.Web.Components.Layout
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<Fluxor.Blazor.Web.StoreInitializer />
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
<AuthorizeRouteView RouteData="@routeData"
|
||||
DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
|
||||
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(Layout.MainLayout)">
|
||||
<p>Sorry, hier gibt’s nichts unter dieser Adresse.</p>
|
||||
<p>Seite nicht gefunden.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
@using Microsoft.AspNetCore.Antiforgery
|
||||
@inject IAntiforgery Antiforgery
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
<form method="post" action="/auth/logout">
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_token" />
|
||||
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
|
||||
<MudButton ButtonType="ButtonType.Submit">@Text</MudButton>
|
||||
</form>
|
||||
|
||||
@code {
|
||||
[Parameter] public string ReturnUrl { get; set; } = "/account/login";
|
||||
[Parameter] public string CssClass { get; set; } = "btn btn-link";
|
||||
[Parameter] public string Text { get; set; } = "Abmelden";
|
||||
|
||||
private string _token = string.Empty;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var http = HttpContextAccessor.HttpContext
|
||||
?? throw new InvalidOperationException("Kein HttpContext verfügbar.");
|
||||
|
||||
var tokens = Antiforgery.GetAndStoreTokens(http);
|
||||
_token = tokens.RequestToken ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.NavigateTo("/account/login", true);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,3 +8,14 @@
|
|||
@using Microsoft.JSInterop
|
||||
@using Koogle.Web
|
||||
@using Koogle.Web.Components
|
||||
|
||||
|
||||
@* MudBlazor UI Components *@
|
||||
@using MudBlazor
|
||||
|
||||
@* Fluxor State Management *@
|
||||
@using Fluxor
|
||||
@using Fluxor.Blazor.Web.Components
|
||||
|
||||
@* Koogle Components *@
|
||||
@using Koogle.Web.Components.Shared
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Koogle.Web.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller für Authentifizierungsaktionen (Login, Logout)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UserManagerService nutzt SignInManager der wiederum nicht in einer SignalR methode funktioniert.
|
||||
/// Aus dem Grund werden Login/Logout Aktionen hier im MVC Controller mit einem "normalen" Post-Request abgewickelt.
|
||||
/// </remarks>
|
||||
[Route("auth")]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public AuthController(IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(
|
||||
[FromForm] LoginDto input
|
||||
)
|
||||
{
|
||||
var returnUrl = string.IsNullOrWhiteSpace(input.ReturnUrl) ? "/" : input.ReturnUrl;
|
||||
|
||||
var user = await _userService.LoginAsync(input);
|
||||
if (user != null)
|
||||
{
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
return LocalRedirect("/account/login?error=true");
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("logout")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout([FromForm] string? returnUrl = null)
|
||||
{
|
||||
await _userService.SignOutAsync();
|
||||
|
||||
// Nach Logout per Redirect neu laden (neuer Request -> neue Auth-Session)
|
||||
returnUrl ??= "/account/login";
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,21 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fluxor.Blazor.Web" Version="6.9.0" />
|
||||
<PackageReference Include="Fluxor.Blazor.Web.ReduxDevTools" Version="6.9.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Koogle.Application\Koogle.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Store\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
using Fluxor;
|
||||
using Fluxor.Blazor.Web.ReduxDevTools;
|
||||
using Koogle.Application;
|
||||
using Koogle.Infrastructure;
|
||||
using Koogle.Infrastructure.Security;
|
||||
using Koogle.Web.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using MudBlazor.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -13,12 +18,30 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
|||
builder.Services.AddApplication();
|
||||
|
||||
|
||||
// Fluxor State Management
|
||||
builder.Services.AddFluxor(options =>
|
||||
{
|
||||
//options.UseRouting();
|
||||
options.ScanAssemblies(typeof(Program).Assembly);
|
||||
#if DEBUG
|
||||
options.UseReduxDevTools();
|
||||
#endif
|
||||
});
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddControllersWithViews();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
// https://dev.to/masanori_msl/blazor-server-signin-with-custom-user-2e74
|
||||
builder.Services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp =>
|
||||
(ServerAuthenticationStateProvider)sp.GetRequiredService<AuthenticationStateProvider>()
|
||||
);
|
||||
|
||||
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
@ -32,7 +55,7 @@ if (!app.Environment.IsDevelopment())
|
|||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
|
@ -51,7 +74,7 @@ using (var scope = app.Services.CreateScope())
|
|||
{
|
||||
await IdentityRoleSeeder.SeedAsync(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
await BootstrapSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,18 @@
|
|||
{
|
||||
|
||||
"Bootstrap": {
|
||||
"EnableSeeding": true,
|
||||
"CreateDefaultClub": true,
|
||||
"SuperAdmin": {
|
||||
"Email": "ch@koogle.de",
|
||||
"UserName": "ch@koogle.de",
|
||||
"Password": "DEV_only3000!"
|
||||
},
|
||||
"DefaultClub": {
|
||||
"Name": "Koogle Demo Club"
|
||||
}
|
||||
},
|
||||
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
if (UserId is null || Email is null || Code is null)
|
||||
{
|
||||
RedirectManager.RedirectToWithStatus(
|
||||
"Account/Login", "Error: Invalid email change confirmation link.", HttpContext);
|
||||
"Account/LoginAsync", "Error: Invalid email change confirmation link.", HttpContext);
|
||||
}
|
||||
|
||||
var user = await UserManager.FindByIdAsync(UserId);
|
||||
|
|
|
|||
|
|
@ -71,13 +71,13 @@
|
|||
{
|
||||
if (RemoteError is not null)
|
||||
{
|
||||
RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
|
||||
RedirectManager.RedirectToWithStatus("Account/LoginAsync", $"Error from external provider: {RemoteError}", HttpContext);
|
||||
}
|
||||
|
||||
var info = await SignInManager.GetExternalLoginInfoAsync();
|
||||
if (info is null)
|
||||
{
|
||||
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
|
||||
RedirectManager.RedirectToWithStatus("Account/LoginAsync", "Error loading external login information.", HttpContext);
|
||||
}
|
||||
|
||||
externalLoginInfo = info;
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
// We should only reach this page via the login callback, so redirect back to
|
||||
// the login page if we get here some other way.
|
||||
RedirectManager.RedirectTo("Account/Login");
|
||||
RedirectManager.RedirectTo("Account/LoginAsync");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
{
|
||||
if (externalLoginInfo is null)
|
||||
{
|
||||
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
|
||||
RedirectManager.RedirectToWithStatus("Account/LoginAsync", "Error loading external login information.", HttpContext);
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
{
|
||||
if (externalLoginInfo is null)
|
||||
{
|
||||
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information during confirmation.", HttpContext);
|
||||
RedirectManager.RedirectToWithStatus("Account/LoginAsync", "Error loading external login information during confirmation.", HttpContext);
|
||||
}
|
||||
|
||||
var emailStore = GetEmailStore();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@page "/Account/Login"
|
||||
@page "/Account/LoginAsync"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
<p>
|
||||
Don't have access to your authenticator device? You can
|
||||
<a href="Account/LoginWithRecoveryCode?ReturnUrl=@ReturnUrl">log in with a recovery code</a>.
|
||||
<a href="Account/LoginAsyncWithRecoveryCode?ReturnUrl=@ReturnUrl">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@code {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<StatusMessage />
|
||||
@if (currentLogins?.Count > 0)
|
||||
{
|
||||
<h3>Registered Logins</h3>
|
||||
<h3>Registered LoginAsyncs</h3>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@foreach (var login in currentLogins)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<MudLink Color="Color.Inherit" Href="/Account/Login">Log in</MudLink>
|
||||
<MudLink Color="Color.Inherit" Href="/Account/LoginAsync">Log in</MudLink>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
|
||||
NavigationManager.NavigateTo($"Account/LoginAsync?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ Welcome to your new app.
|
|||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="/account/Register">Register</a>
|
||||
<a href="/account/Login">Login</a>
|
||||
<a href="/account/Login">LoginAsync</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
|
|
|
|||
Loading…
Reference in New Issue