App login is working
This commit is contained in:
@@ -1,60 +1,51 @@
|
|||||||
using Google.Apis.Auth;
|
using DiunaBI.API.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using DiunaBI.Domain.Entities;
|
using DiunaBI.Domain.Entities;
|
||||||
using DiunaBI.Infrastructure.Data;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DiunaBI.API.Controllers;
|
namespace DiunaBI.API.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
// [Authorize]
|
public class AuthController(
|
||||||
public class AuthController : Controller
|
GoogleAuthService googleAuthService,
|
||||||
|
JwtTokenService jwtTokenService,
|
||||||
|
ILogger<AuthController> logger)
|
||||||
|
: ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
[HttpPost("apiToken")]
|
||||||
private readonly IConfiguration _configuration;
|
public async Task<IActionResult> ApiToken([FromBody] string idToken)
|
||||||
public AuthController(
|
|
||||||
AppDbContext db, IConfiguration configuration)
|
|
||||||
{ _db = db; _configuration = configuration; }
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Route("apiToken")]
|
|
||||||
public async Task<IActionResult> ApiToken([FromBody] string credential)
|
|
||||||
{
|
{
|
||||||
var settings = new GoogleJsonWebSignature.ValidationSettings
|
try
|
||||||
{
|
{
|
||||||
Audience = new List<string> { _configuration.GetValue<string>("GoogleClientId")! }
|
if (string.IsNullOrEmpty(idToken))
|
||||||
};
|
|
||||||
var payload = await GoogleJsonWebSignature.ValidateAsync(credential, settings);
|
|
||||||
var user = _db.Users.AsNoTracking().FirstOrDefault(x => x.Email == payload.Email);
|
|
||||||
return user != null ? (IActionResult)Ok(JwtGenerator(user)) : Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
private dynamic JwtGenerator(User user)
|
|
||||||
{
|
|
||||||
var key = Encoding.ASCII.GetBytes(_configuration.GetValue<string>("Secret")!);
|
|
||||||
var expirationTime = DateTime.UtcNow.AddMinutes(5);
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
|
||||||
{
|
|
||||||
Subject = new ClaimsIdentity(new[]
|
|
||||||
{
|
{
|
||||||
new Claim("Id", Guid.NewGuid().ToString()),
|
logger.LogWarning("Empty idToken received");
|
||||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
return BadRequest("IdToken is required");
|
||||||
new Claim(JwtRegisteredClaimNames.Jti,
|
}
|
||||||
Guid.NewGuid().ToString())
|
|
||||||
}),
|
var (isValid, user, error) = await googleAuthService.ValidateGoogleTokenAsync(idToken);
|
||||||
Expires = expirationTime,
|
|
||||||
SigningCredentials = new SigningCredentials
|
if (!isValid || user == null)
|
||||||
(new SymmetricSecurityKey(key),
|
{
|
||||||
SecurityAlgorithms.HmacSha512Signature)
|
logger.LogWarning("Google token validation failed: {Error}", error);
|
||||||
};
|
return Unauthorized();
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
}
|
||||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
||||||
var stringToken = tokenHandler.WriteToken(token);
|
var jwt = jwtTokenService.GenerateToken(user);
|
||||||
return new { token = stringToken, id = user.Id, expirationTime };
|
|
||||||
|
logger.LogInformation("User authenticated successfully: {Email}", user.Email);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
token = jwt,
|
||||||
|
id = user.Id,
|
||||||
|
expirationTime = DateTime.UtcNow.AddDays(7) // z JwtSettings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error during authentication");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using DiunaBI.API.Services;
|
||||||
using DiunaBI.Infrastructure.Data;
|
using DiunaBI.Infrastructure.Data;
|
||||||
using DiunaBI.Infrastructure.Services;
|
using DiunaBI.Infrastructure.Services;
|
||||||
using Google.Apis.Sheets.v4;
|
using Google.Apis.Sheets.v4;
|
||||||
@@ -74,6 +75,9 @@ builder.Services.AddAuthentication(options =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<GoogleAuthService>();
|
||||||
|
builder.Services.AddScoped<JwtTokenService>();
|
||||||
|
|
||||||
// Google Sheets dependencies
|
// Google Sheets dependencies
|
||||||
Console.WriteLine("Adding Google Sheets dependencies...");
|
Console.WriteLine("Adding Google Sheets dependencies...");
|
||||||
builder.Services.AddSingleton<GoogleSheetsHelper>();
|
builder.Services.AddSingleton<GoogleSheetsHelper>();
|
||||||
@@ -159,6 +163,9 @@ app.UseAuthorization();
|
|||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapGet("/health", () => Results.Ok(new { status = "OK", timestamp = DateTime.UtcNow }))
|
||||||
|
.AllowAnonymous();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
if (app.Environment.IsProduction())
|
if (app.Environment.IsProduction())
|
||||||
|
|||||||
60
src/Backend/DiunaBI.API/Services/GoogleAuthService.cs
Normal file
60
src/Backend/DiunaBI.API/Services/GoogleAuthService.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using DiunaBI.Domain.Entities;
|
||||||
|
using DiunaBI.Infrastructure.Data;
|
||||||
|
using Google.Apis.Auth;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DiunaBI.API.Services;
|
||||||
|
|
||||||
|
public class GoogleAuthService(AppDbContext context, IConfiguration configuration, ILogger<GoogleAuthService> logger)
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context = context;
|
||||||
|
private readonly IConfiguration _configuration = configuration;
|
||||||
|
private readonly ILogger<GoogleAuthService> _logger = logger;
|
||||||
|
|
||||||
|
public async Task<(bool IsValid, User? user, string? error)> ValidateGoogleTokenAsync(string idToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var clientId = _configuration["GoogleAuth:ClientId"];
|
||||||
|
if (string.IsNullOrEmpty(clientId))
|
||||||
|
{
|
||||||
|
_logger.LogError("Google Auth Client Id is not configured");
|
||||||
|
return (false, null, "Google Auth Client Id is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = await GoogleJsonWebSignature.ValidateAsync(idToken,
|
||||||
|
new GoogleJsonWebSignature.ValidationSettings
|
||||||
|
{
|
||||||
|
Audience = new[] { clientId }
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("Google token validated for user: {Email}", payload.Email);
|
||||||
|
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Email == payload.Email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("User not found in DiunaBI database: {Email}", payload.Email);
|
||||||
|
return (false, null, "User not found in DiunaBI database");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.UserName = payload.Name;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("User logged in: {Email}", payload.Email);
|
||||||
|
|
||||||
|
return (true, user, null);
|
||||||
|
}
|
||||||
|
catch (InvalidJwtException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Invalid JWT token");
|
||||||
|
return (false, null, "Invalid JWT token");
|
||||||
|
} catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error validating Google token");
|
||||||
|
return (false, null, "Error validating Google token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/Backend/DiunaBI.API/Services/JwtTokenService.cs
Normal file
84
src/Backend/DiunaBI.API/Services/JwtTokenService.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using DiunaBI.Domain.Entities;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace DiunaBI.API.Services;
|
||||||
|
|
||||||
|
public class JwtTokenService(IConfiguration configuration, ILogger<JwtTokenService> logger)
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration = configuration;
|
||||||
|
private readonly ILogger<JwtTokenService> _logger = logger;
|
||||||
|
|
||||||
|
public string GenerateToken(User user)
|
||||||
|
{
|
||||||
|
var jwtSettings = _configuration.GetSection("JwtSettings");
|
||||||
|
var securityKey = jwtSettings["SecurityKey"];
|
||||||
|
var issuer = jwtSettings["Issuer"];
|
||||||
|
var audience = jwtSettings["Audience"];
|
||||||
|
var expiryDays = int.Parse(jwtSettings["ExpiryDays"] ?? "7");
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.Name, user.UserName),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(),
|
||||||
|
ClaimValueTypes.Integer64)
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: issuer,
|
||||||
|
audience: audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddDays(expiryDays),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
_logger.LogInformation("Generated JWT token for user: {Email}", user.Email);
|
||||||
|
|
||||||
|
return tokenString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsPrincipal? ValidateToken(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwtSettings = _configuration.GetSection("JwtSettings");
|
||||||
|
var secretKey = jwtSettings["SecretKey"];
|
||||||
|
var issuer = jwtSettings["Issuer"];
|
||||||
|
var audience = jwtSettings["Audience"];
|
||||||
|
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var key = Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
|
||||||
|
var validationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = issuer,
|
||||||
|
ValidAudience = audience,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
|
||||||
|
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error validating JWT token");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,9 @@
|
|||||||
"SQLDatabase": "#{db-connection-string}#"
|
"SQLDatabase": "#{db-connection-string}#"
|
||||||
},
|
},
|
||||||
"InstanceName": "#{app-environment}#",
|
"InstanceName": "#{app-environment}#",
|
||||||
"GoogleClientId": "#{google-backend-login-client-id}#",
|
"GoogleAuth": {
|
||||||
|
"ClientId": "#{google-backend-login-client-id}#"
|
||||||
|
},
|
||||||
"Secret": "#{google-backend-login-secret}#",
|
"Secret": "#{google-backend-login-secret}#",
|
||||||
"apiKey": "#{api-key}#",
|
"apiKey": "#{api-key}#",
|
||||||
"powerBI-user": "#{powerBI-user}#",
|
"powerBI-user": "#{powerBI-user}#",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class User
|
|||||||
#region Properties
|
#region Properties
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public string? Email { get; init; }
|
public string? Email { get; init; }
|
||||||
public string? UserName { get; init; }
|
public string? UserName { get; set; }
|
||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,44 @@
|
|||||||
@page "/dashboard"
|
@page "/dashboard"
|
||||||
|
@using DiunaBI.UI.Shared.Services
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
|
@inject AuthService AuthService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<MudText Typo="Typo.h4">Dashboard</MudText>
|
<MudText Typo="Typo.body1" Class="mt-4">Witaj w DiunaBI!</MudText>
|
||||||
<p>Tutaj znajdzie się panel ogólny aplikacji</p>
|
|
||||||
|
@if (AuthService.IsAuthenticated && AuthService.CurrentUser != null)
|
||||||
|
{
|
||||||
|
<MudCard Class="mt-4" Elevation="2">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderAvatar>
|
||||||
|
@if (!string.IsNullOrEmpty(AuthService.CurrentUser.AvatarUrl))
|
||||||
|
{
|
||||||
|
<MudAvatar Size="Size.Large" Style="background: transparent;">
|
||||||
|
<img src="@AuthService.CurrentUser.AvatarUrl" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAvatar Color="Color.Primary" Size="Size.Large">
|
||||||
|
@(AuthService.CurrentUser.FullName.Length > 0 ? AuthService.CurrentUser.FullName.Substring(0, 1) : "?")
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
</CardHeaderAvatar>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h6">@AuthService.CurrentUser.FullName</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@AuthService.CurrentUser.Email</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
✅ Zalogowano przez Google
|
||||||
|
</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Class="mt-4">
|
||||||
|
Nie jesteś zalogowany
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<MudCard Class="login-card" Elevation="8">
|
<MudCard Class="login-card" Elevation="8">
|
||||||
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">Witaj w BimAI</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">Witaj w DiunaBI</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="mb-6 text-center">
|
<MudText Typo="Typo.body1" Class="mb-6 text-center">
|
||||||
Zaloguj się używając konta Google
|
Zaloguj się używając konta Google
|
||||||
</MudText>
|
</MudText>
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
@if (_isLoading)
|
@if (_isLoading)
|
||||||
{
|
{
|
||||||
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
||||||
|
<span>Weryfikacja...</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Error" Class="mt-4">
|
<MudAlert Severity="Severity.Error" Class="mt-4" Dense="true">
|
||||||
@_errorMessage
|
@_errorMessage
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
}
|
}
|
||||||
@@ -94,27 +95,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public static async Task OnGoogleSignInSuccess(string accessToken, string fullName, string email, string avatarUrl)
|
public static async Task OnGoogleSignInSuccess(string googleCredential, string fullName, string email, string avatarUrl)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Google Sign-In Success: {email}");
|
Console.WriteLine($"=== OnGoogleSignInSuccess: {email} ===");
|
||||||
|
|
||||||
if (_instance != null)
|
if (_instance != null)
|
||||||
{
|
{
|
||||||
var userInfo = new UserInfo
|
try
|
||||||
{
|
{
|
||||||
FullName = fullName,
|
// Waliduj użytkownika w backendzie DiunaBI
|
||||||
Email = email,
|
var (success, errorMessage) = await _instance.AuthService.ValidateWithBackendAsync(
|
||||||
AvatarUrl = avatarUrl
|
googleCredential, fullName, email, avatarUrl);
|
||||||
};
|
|
||||||
|
|
||||||
await _instance.AuthService.SetAuthenticationAsync(accessToken, userInfo);
|
if (success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("✅ User validated, navigating to dashboard");
|
||||||
|
_instance._isLoading = false;
|
||||||
|
_instance._errorMessage = string.Empty;
|
||||||
|
_instance.NavigationManager.NavigateTo("/dashboard", replace: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Validation failed: {errorMessage}");
|
||||||
|
_instance._isLoading = false;
|
||||||
|
_instance._errorMessage = errorMessage ?? "Nie udało się zalogować.";
|
||||||
|
}
|
||||||
|
|
||||||
_instance._isLoading = false;
|
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||||
_instance._errorMessage = string.Empty;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
_instance.NavigationManager.NavigateTo("/dashboard", replace: true);
|
{
|
||||||
|
Console.Error.WriteLine($"❌ OnGoogleSignInSuccess error: {ex.Message}");
|
||||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
_instance._isLoading = false;
|
||||||
|
_instance._errorMessage = "Błąd podczas weryfikacji użytkownika.";
|
||||||
|
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
OnClick="ToggleDrawer"
|
OnClick="ToggleDrawer"
|
||||||
Class="mud-hidden-md-up"/>
|
Class="mud-hidden-md-up"/>
|
||||||
<MudSpacer/>
|
<MudSpacer/>
|
||||||
<MudText Typo="Typo.h6">BimAI</MudText>
|
<MudText Typo="Typo.h6">DiunaBI</MudText>
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
|
||||||
<MudDrawer @bind-Open="_drawerOpen"
|
<MudDrawer @bind-Open="_drawerOpen"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/BimAI.UI.Shared/images/login-background.jpg') no-repeat center;
|
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/DiunaBI.UI.Shared/images/login-background.jpg') no-repeat center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
@@ -5,90 +6,153 @@ namespace DiunaBI.UI.Shared.Services;
|
|||||||
|
|
||||||
public class UserInfo
|
public class UserInfo
|
||||||
{
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
public string FullName { get; set; } = string.Empty;
|
public string FullName { get; set; } = string.Empty;
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
public string AvatarUrl { get; set; } = string.Empty;
|
public string AvatarUrl { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
public class AuthService
|
public class AuthService
|
||||||
{
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
private readonly IJSRuntime _jsRuntime;
|
private readonly IJSRuntime _jsRuntime;
|
||||||
private bool? _isAuthenticated;
|
private bool? _isAuthenticated;
|
||||||
private UserInfo? _userInfo = null;
|
private UserInfo? _userInfo = null;
|
||||||
|
private string? _apiToken;
|
||||||
|
|
||||||
public event Action<bool>? AuthenticationStateChanged;
|
public event Action<bool>? AuthenticationStateChanged;
|
||||||
|
|
||||||
public AuthService(IJSRuntime jsRuntime)
|
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_jsRuntime = jsRuntime;
|
_jsRuntime = jsRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAuthenticated => _isAuthenticated ?? false;
|
public bool IsAuthenticated => _isAuthenticated ?? false;
|
||||||
public UserInfo? CurrentUser => _userInfo;
|
public UserInfo? CurrentUser => _userInfo;
|
||||||
|
|
||||||
|
public async Task<(bool success, string? errorMessage)> ValidateWithBackendAsync(string googleCredential, string fullName, string email, string avatarUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"=== ValidateWithBackend: Sending Google credential for {email} ===");
|
||||||
|
|
||||||
|
// Wyślij Google credential do backendu
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("/api/Auth/apiToken", googleCredential);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiTokenResponse>();
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
_apiToken = result.Token;
|
||||||
|
_userInfo = new UserInfo
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
FullName = fullName,
|
||||||
|
Email = email,
|
||||||
|
AvatarUrl = avatarUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zapisz do localStorage
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "api_token", _apiToken);
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", JsonSerializer.Serialize(_userInfo));
|
||||||
|
|
||||||
|
// Ustaw header dla przyszłych requestów
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
||||||
|
|
||||||
|
_isAuthenticated = true;
|
||||||
|
Console.WriteLine($"✅ Backend validation successful. UserId={result.Id}");
|
||||||
|
AuthenticationStateChanged?.Invoke(true);
|
||||||
|
|
||||||
|
return (true, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ User not found in DiunaBI database");
|
||||||
|
return (false, "Użytkownik nie istnieje w bazie DiunaBI.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Backend error: {response.StatusCode}");
|
||||||
|
return (false, "Błąd serwera DiunaBI. Spróbuj ponownie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, "Nieoczekiwany błąd.");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Network error: {ex.Message}");
|
||||||
|
return (false, "Nie można połączyć się z serwerem DiunaBI.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Validation error: {ex.Message}");
|
||||||
|
return (false, "Błąd podczas weryfikacji użytkownika.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckAuthenticationAsync()
|
public async Task<bool> CheckAuthenticationAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
Console.WriteLine("=== AuthService.CheckAuthenticationAsync START ===");
|
||||||
|
|
||||||
|
var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "api_token");
|
||||||
var userInfoJson = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "user_info");
|
var userInfoJson = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "user_info");
|
||||||
|
|
||||||
_isAuthenticated = !string.IsNullOrEmpty(token);
|
_isAuthenticated = !string.IsNullOrEmpty(token);
|
||||||
|
|
||||||
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
|
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
|
||||||
{
|
{
|
||||||
|
_apiToken = token;
|
||||||
_userInfo = JsonSerializer.Deserialize<UserInfo>(userInfoJson);
|
_userInfo = JsonSerializer.Deserialize<UserInfo>(userInfoJson);
|
||||||
|
|
||||||
|
// Przywróć header
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ No valid session");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"AuthService.CheckAuthentication: token={(!string.IsNullOrEmpty(token) ? "EXISTS" : "NULL")}, isAuth={_isAuthenticated}");
|
Console.WriteLine($"=== AuthService.CheckAuthenticationAsync END (authenticated={_isAuthenticated}) ===");
|
||||||
|
|
||||||
return _isAuthenticated.Value;
|
return _isAuthenticated.Value;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AuthService.CheckAuthentication ERROR: {ex.Message}");
|
Console.WriteLine($"❌ CheckAuthentication ERROR: {ex.Message}");
|
||||||
_isAuthenticated = false;
|
_isAuthenticated = false;
|
||||||
_userInfo = null;
|
_userInfo = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetAuthenticationAsync(string token, UserInfo? userInfo = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "google_token", token);
|
|
||||||
|
|
||||||
if (userInfo != null)
|
|
||||||
{
|
|
||||||
_userInfo = userInfo;
|
|
||||||
var userInfoJson = JsonSerializer.Serialize(userInfo);
|
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", userInfoJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isAuthenticated = true;
|
|
||||||
Console.WriteLine($"AuthService.SetAuthentication: token saved, user={_userInfo?.Email}");
|
|
||||||
AuthenticationStateChanged?.Invoke(true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"AuthService.SetAuthentication ERROR: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearAuthenticationAsync()
|
public async Task ClearAuthenticationAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "google_token");
|
Console.WriteLine("=== AuthService.ClearAuthenticationAsync ===");
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "api_token");
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
||||||
|
|
||||||
|
_apiToken = null;
|
||||||
_isAuthenticated = false;
|
_isAuthenticated = false;
|
||||||
_userInfo = null;
|
_userInfo = null;
|
||||||
Console.WriteLine($"AuthService.ClearAuthentication: token and user ingfo removed");
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
|
||||||
|
Console.WriteLine("✅ Authentication cleared");
|
||||||
AuthenticationStateChanged?.Invoke(false);
|
AuthenticationStateChanged?.Invoke(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AuthService.ClearAuthentication ERROR: {ex.Message}");
|
Console.WriteLine($"❌ ClearAuthentication ERROR: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,15 +163,13 @@ public class AuthService
|
|||||||
await CheckAuthenticationAsync();
|
await CheckAuthenticationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isAuthenticated != true) return null;
|
return _apiToken;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
private class ApiTokenResponse
|
||||||
{
|
{
|
||||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
public string Token { get; set; } = string.Empty;
|
||||||
}
|
public Guid Id { get; set; }
|
||||||
catch
|
public DateTime ExpirationTime { get; set; }
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,119 +1,98 @@
|
|||||||
let googleClient = null;
|
let googleInitialized = false;
|
||||||
let isSigningIn = false;
|
|
||||||
|
|
||||||
function waitForGoogleApi() {
|
window.initGoogleSignIn = function(clientId) {
|
||||||
return new Promise((resolve, reject) => {
|
if (googleInitialized) {
|
||||||
if (window.google?.accounts?.oauth2) {
|
console.log("Google Sign-In already initialized");
|
||||||
resolve(window.google);
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
console.log("🔐 Initializing Google Sign-In (ID Token flow) with clientId:", clientId);
|
||||||
|
|
||||||
|
// Używamy google.accounts.id - zwraca ID token (JWT credential)
|
||||||
|
google.accounts.id.initialize({
|
||||||
|
client_id: clientId,
|
||||||
|
callback: handleCredentialResponse,
|
||||||
|
auto_select: false,
|
||||||
|
cancel_on_tap_outside: true
|
||||||
|
});
|
||||||
|
|
||||||
|
googleInitialized = true;
|
||||||
|
console.log("✅ Google Sign-In initialized successfully");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.requestGoogleSignIn = function() {
|
||||||
|
console.log("🚀 Requesting Google Sign-In...");
|
||||||
|
|
||||||
|
// Pokaż One Tap prompt
|
||||||
|
google.accounts.id.prompt((notification) => {
|
||||||
|
if (notification.isNotDisplayed()) {
|
||||||
|
console.warn("⚠️ One Tap not displayed:", notification.getNotDisplayedReason());
|
||||||
|
} else if (notification.isSkippedMoment()) {
|
||||||
|
console.warn("⚠️ One Tap skipped:", notification.getSkippedReason());
|
||||||
|
} else {
|
||||||
|
console.log("✅ One Tap displayed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxAttempts = 20;
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
const checkGoogle = setInterval(() => {
|
|
||||||
attempts++;
|
|
||||||
if (window.google?.accounts?.oauth2) {
|
|
||||||
clearInterval(checkGoogle);
|
|
||||||
resolve(window.google);
|
|
||||||
} else if (attempts >= maxAttempts) {
|
|
||||||
clearInterval(checkGoogle);
|
|
||||||
reject(new Error('Google OAuth2 API failed to load within the timeout period'));
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
async function handleAuthError(error, context = '') {
|
function handleCredentialResponse(response) {
|
||||||
const errorMessage = error?.message || error?.type || error?.toString() || 'Unknown error';
|
console.log("=== 🎉 Google Credential Response ===");
|
||||||
const fullError = `${context}: ${errorMessage}`;
|
|
||||||
console.error('Google Auth Error:', { context, error, fullError });
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError', fullError);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUserInfo(accessToken) {
|
|
||||||
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
|
||||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('Failed to fetch user info:', errorText);
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
|
||||||
`Failed to fetch user info: HTTP ${response.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.initGoogleSignIn = async function(clientId) {
|
|
||||||
if (googleClient) {
|
|
||||||
return googleClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const google = await waitForGoogleApi();
|
if (!response.credential) {
|
||||||
|
throw new Error("No credential in response");
|
||||||
|
}
|
||||||
|
|
||||||
googleClient = google.accounts.oauth2.initTokenClient({
|
const tokenParts = response.credential.split('.');
|
||||||
client_id: clientId,
|
console.log("📝 ID Token parts:", tokenParts.length); // Should be 3 (JWT)
|
||||||
scope: 'email profile',
|
console.log("📏 ID Token length:", response.credential.length);
|
||||||
callback: async (tokenResponse) => {
|
|
||||||
try {
|
|
||||||
if (tokenResponse.error) {
|
|
||||||
console.error('Token response error:', tokenResponse.error);
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
|
||||||
tokenResponse.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = await fetchUserInfo(tokenResponse.access_token);
|
if (tokenParts.length !== 3) {
|
||||||
if (!userInfo) return;
|
throw new Error("Invalid JWT format - expected 3 parts (header.payload.signature)");
|
||||||
|
}
|
||||||
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInSuccess',
|
// Dekoduj payload JWT aby wyciągnąć user info
|
||||||
tokenResponse.access_token,
|
const payload = decodeJwtPayload(response.credential);
|
||||||
userInfo.name || '',
|
|
||||||
userInfo.email || '',
|
|
||||||
userInfo.picture || ''
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Callback error:', error);
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
|
||||||
error.message || 'Unknown callback error');
|
|
||||||
} finally {
|
|
||||||
isSigningIn = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error_callback: async (error) => {
|
|
||||||
console.error('OAuth flow error:', error);
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
|
||||||
error.type || 'OAuth flow error');
|
|
||||||
isSigningIn = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return googleClient;
|
const fullName = payload.name || `${payload.given_name || ''} ${payload.family_name || ''}`.trim();
|
||||||
|
const email = payload.email;
|
||||||
|
const avatarUrl = payload.picture || '';
|
||||||
|
|
||||||
|
console.log("👤 User info from JWT:", { fullName, email });
|
||||||
|
console.log("📧 Email verified:", payload.email_verified);
|
||||||
|
|
||||||
|
// Wywołaj Blazor - przekaż ID token JWT (nie access token!)
|
||||||
|
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInSuccess',
|
||||||
|
response.credential, // <--- To jest ID token JWT dla backendu
|
||||||
|
fullName,
|
||||||
|
email,
|
||||||
|
avatarUrl)
|
||||||
|
.then(() => {
|
||||||
|
console.log("✅ Successfully sent ID token to Blazor");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("❌ Error calling Blazor:", err);
|
||||||
|
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInError', err.toString());
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Initiaxcrun xctrace list deviceslization error:', error);
|
console.error("❌ Error processing Google credential:", error);
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInError', error.toString());
|
||||||
error.message || 'Failed to initialize Google Sign-In');
|
|
||||||
isSigningIn = false;
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
window.requestGoogleSignIn = async function() {
|
function decodeJwtPayload(token) {
|
||||||
if (isSigningIn) {
|
try {
|
||||||
console.log('Sign-in already in progress');
|
const base64Url = token.split('.')[1];
|
||||||
return;
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(
|
||||||
|
atob(base64)
|
||||||
|
.split('')
|
||||||
|
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error decoding JWT:", error);
|
||||||
|
throw new Error("Invalid JWT format");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!googleClient) {
|
|
||||||
console.error('Google Sign-In not initialized');
|
|
||||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
|
||||||
'Google Sign-In not initialized. Call initGoogleSignIn first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSigningIn = true;
|
|
||||||
googleClient.requestAccessToken();
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
<link href="BimAI.UI.Web.styles.css" rel="stylesheet" />
|
<link href="DiunaBI.UI.Web.styles.css" rel="stylesheet" />
|
||||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
<HeadOutlet />
|
<HeadOutlet />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
###
|
###
|
||||||
GET http://localhost:5400/api/Layers/ProcessQueue/10763478CB738D4ecb2h76g803478CB738D4e
|
GET http://localhost:5400/health
|
||||||
|
|||||||
Reference in New Issue
Block a user