Implement Google authentication (for Web) and user management system
This commit is contained in:
@@ -24,11 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Pages\ProductList.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
<Folder Include="wwwroot\images\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
43
Bimix.UI.Shared/Components/AuthGuard.razor
Normal file
43
Bimix.UI.Shared/Components/AuthGuard.razor
Normal file
@@ -0,0 +1,43 @@
|
||||
@using Bimix.UI.Shared.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="d-flex justify-center align-center" style="height: 100vh;">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
|
||||
</div>
|
||||
}
|
||||
else if (_isAuthenticated)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private bool _isLoading = true;
|
||||
private bool _isAuthenticated = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Console.WriteLine("AuthGuard: Checking authentication...");
|
||||
|
||||
// ZAWSZE sprawdź localStorage przy inicjalizacji
|
||||
_isAuthenticated = await AuthService.CheckAuthenticationAsync();
|
||||
_isLoading = false;
|
||||
|
||||
Console.WriteLine($"AuthGuard: isAuthenticated={_isAuthenticated}");
|
||||
|
||||
if (!_isAuthenticated)
|
||||
{
|
||||
Console.WriteLine("AuthGuard: Redirecting to /login");
|
||||
Navigation.NavigateTo("/login", replace: true);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
121
Bimix.UI.Shared/Components/LoginCard.razor
Normal file
121
Bimix.UI.Shared/Components/LoginCard.razor
Normal file
@@ -0,0 +1,121 @@
|
||||
@using Microsoft.Extensions.Configuration
|
||||
@using Bimix.UI.Shared.Services
|
||||
@inject IJSRuntime JS
|
||||
@inject IConfiguration Configuration
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<MudCard Class="login-card" Elevation="8">
|
||||
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Witaj w Bimix</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-6 text-center">
|
||||
Zaloguj się używając konta Google
|
||||
</MudText>
|
||||
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Custom.Brands.Google"
|
||||
Size="Size.Large"
|
||||
OnClick="HandleGoogleSignIn"
|
||||
Disabled="@_isLoading">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Zaloguj z Google</span>
|
||||
}
|
||||
</MudButton>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">
|
||||
@_errorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
@code {
|
||||
private bool _isLoading = false;
|
||||
private string _errorMessage = string.Empty;
|
||||
private static LoginCard? _instance;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
private async Task HandleGoogleSignIn()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
var clientId = Configuration["GoogleAuth:ClientId"];
|
||||
|
||||
if (string.IsNullOrEmpty(clientId))
|
||||
{
|
||||
throw new Exception("Google ClientId is not configured.");
|
||||
}
|
||||
|
||||
|
||||
await JS.InvokeVoidAsync("initGoogleSignIn", clientId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = "Błąd podczas logownia. Spróbuj ponownie";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public static async Task OnGoogleSignInSuccess(string idToken)
|
||||
{
|
||||
Console.WriteLine($"Google ID Token: {idToken}");
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
await _instance.AuthService.SetAuthenticationAsync(idToken);
|
||||
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = string.Empty;
|
||||
|
||||
_instance.NavigationManager.NavigateTo("/dashboard", replace:true);
|
||||
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public static async Task OnGoggleSignInError(string error)
|
||||
{
|
||||
Console.WriteLine($"Google SignIn Error: {error}");
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = "Błąd logowanie Google. Spróbuj ponownie";
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<style>
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.google-signin-button {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
</style>
|
||||
8
Bimix.UI.Shared/EmptyLayout.razor
Normal file
8
Bimix.UI.Shared/EmptyLayout.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
|
||||
@Body
|
||||
@@ -1,41 +1,43 @@
|
||||
@using MudBlazor
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
<AuthGuard>
|
||||
<MudThemeProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
<MudLayout>
|
||||
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
||||
<MudAppBar Elevation="0">
|
||||
<MudIconButton
|
||||
Icon="@Icons.Material.Filled.Menu"
|
||||
Color="Color.Inherit"
|
||||
Edge="Edge.Start"
|
||||
OnClick="ToggleDrawer"
|
||||
Class="mud-hidden-md-up"/>
|
||||
<MudSpacer/>
|
||||
<MudText Typo="Typo.h6">Bimix</MudText>
|
||||
</MudAppBar>
|
||||
<MudLayout>
|
||||
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
||||
<MudAppBar Elevation="0">
|
||||
<MudIconButton
|
||||
Icon="@Icons.Material.Filled.Menu"
|
||||
Color="Color.Inherit"
|
||||
Edge="Edge.Start"
|
||||
OnClick="ToggleDrawer"
|
||||
Class="mud-hidden-md-up"/>
|
||||
<MudSpacer/>
|
||||
<MudText Typo="Typo.h6">Bimix</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-Open="_drawerOpen"
|
||||
Anchor="Anchor.Start"
|
||||
Variant="@_drawerVariant"
|
||||
Elevation="1"
|
||||
ClipMode="DrawerClipMode.Always"
|
||||
Class="mud-width-250">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||
<MudNavLink Href="/products" Icon="@Icons.Material.Filled.Inventory">Products</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
<MudDrawer @bind-Open="_drawerOpen"
|
||||
Anchor="Anchor.Start"
|
||||
Variant="@_drawerVariant"
|
||||
Elevation="1"
|
||||
ClipMode="DrawerClipMode.Always"
|
||||
Class="mud-width-250">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||
<MudNavLink Href="/products" Icon="@Icons.Material.Filled.Inventory">Products</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
</AuthGuard>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -60,7 +62,8 @@
|
||||
_drawerVariant = DrawerVariant.Persistent;
|
||||
_drawerOpen = true;
|
||||
}
|
||||
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
46
Bimix.UI.Shared/Pages/LoginPage.razor
Normal file
46
Bimix.UI.Shared/Pages/LoginPage.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
@page "/login"
|
||||
@layout EmptyLayout
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<LoginCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/Bimix.UI.Shared/images/login-background.jpg') no-repeat center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
</style>
|
||||
86
Bimix.UI.Shared/Services/AuthService.cs
Normal file
86
Bimix.UI.Shared/Services/AuthService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Bimix.UI.Shared.Services;
|
||||
|
||||
public class AuthService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private bool? _isAuthenticated;
|
||||
|
||||
public event Action<bool>? AuthenticationStateChanged;
|
||||
|
||||
public AuthService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public bool IsAuthenticated => _isAuthenticated ?? false;
|
||||
|
||||
public async Task<bool> CheckAuthenticationAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
||||
_isAuthenticated = !string.IsNullOrEmpty(token);
|
||||
|
||||
Console.WriteLine($"AuthService.CheckAuthentication: token={(!string.IsNullOrEmpty(token) ? "EXISTS" : "NULL")}, isAuth={_isAuthenticated}");
|
||||
|
||||
return _isAuthenticated.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AuthService.CheckAuthentication ERROR: {ex.Message}");
|
||||
_isAuthenticated = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAuthenticationAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "google_token", token);
|
||||
_isAuthenticated = true;
|
||||
Console.WriteLine($"AuthService.SetAuthentication: token saved, isAuth={_isAuthenticated}");
|
||||
AuthenticationStateChanged?.Invoke(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AuthService.SetAuthentication ERROR: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearAuthenticationAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "google_token");
|
||||
_isAuthenticated = false;
|
||||
Console.WriteLine($"AuthService.ClearAuthentication: token removed");
|
||||
AuthenticationStateChanged?.Invoke(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AuthService.ClearAuthentication ERROR: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetTokenAsync()
|
||||
{
|
||||
if (_isAuthenticated != true)
|
||||
{
|
||||
await CheckAuthenticationAsync();
|
||||
}
|
||||
|
||||
if (_isAuthenticated != true) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Bimix.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
7
Bimix.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bimix.UI.Shared.Services;
|
||||
|
||||
// TODO it's a good place for this file?
|
||||
public class GoogleAuthConfig
|
||||
{
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
}
|
||||
BIN
Bimix.UI.Shared/wwwroot/images/login-background.jpg
Normal file
BIN
Bimix.UI.Shared/wwwroot/images/login-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 965 KiB |
35
Bimix.UI.Shared/wwwroot/js/auth.js
Normal file
35
Bimix.UI.Shared/wwwroot/js/auth.js
Normal file
@@ -0,0 +1,35 @@
|
||||
window.initGoogleSignIn = async function(clientId) {
|
||||
try {
|
||||
if (!clientId) {
|
||||
throw new Error('ClientId is required');
|
||||
}
|
||||
|
||||
// Inicjalizacja Google Sign-In z dynamicznym ClientId
|
||||
google.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
callback: handleGoogleResponse,
|
||||
auto_select: false,
|
||||
cancel_on_tap_outside: true
|
||||
});
|
||||
|
||||
// Wyświetl popup logowania
|
||||
google.accounts.id.prompt((notification) => {
|
||||
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
||||
console.log('Google Sign-In popup not displayed');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Google Sign-In initialization error:', error);
|
||||
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInError', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
function handleGoogleResponse(response) {
|
||||
if (response.credential) {
|
||||
// Token otrzymany - wyślij do Blazor
|
||||
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInSuccess', response.credential);
|
||||
} else {
|
||||
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInError', 'No credential received');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user