App login is working
This commit is contained in:
@@ -1,60 +1,51 @@
|
||||
using Google.Apis.Auth;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using DiunaBI.API.Services;
|
||||
using DiunaBI.Domain.Entities;
|
||||
using DiunaBI.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DiunaBI.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
// [Authorize]
|
||||
public class AuthController : Controller
|
||||
public class AuthController(
|
||||
GoogleAuthService googleAuthService,
|
||||
JwtTokenService jwtTokenService,
|
||||
ILogger<AuthController> logger)
|
||||
: ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConfiguration _configuration;
|
||||
public AuthController(
|
||||
AppDbContext db, IConfiguration configuration)
|
||||
{ _db = db; _configuration = configuration; }
|
||||
|
||||
[HttpPost]
|
||||
[Route("apiToken")]
|
||||
public async Task<IActionResult> ApiToken([FromBody] string credential)
|
||||
[HttpPost("apiToken")]
|
||||
public async Task<IActionResult> ApiToken([FromBody] string idToken)
|
||||
{
|
||||
var settings = new GoogleJsonWebSignature.ValidationSettings
|
||||
try
|
||||
{
|
||||
Audience = new List<string> { _configuration.GetValue<string>("GoogleClientId")! }
|
||||
};
|
||||
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[]
|
||||
if (string.IsNullOrEmpty(idToken))
|
||||
{
|
||||
new Claim("Id", Guid.NewGuid().ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Jti,
|
||||
Guid.NewGuid().ToString())
|
||||
}),
|
||||
Expires = expirationTime,
|
||||
SigningCredentials = new SigningCredentials
|
||||
(new SymmetricSecurityKey(key),
|
||||
SecurityAlgorithms.HmacSha512Signature)
|
||||
};
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
var stringToken = tokenHandler.WriteToken(token);
|
||||
return new { token = stringToken, id = user.Id, expirationTime };
|
||||
logger.LogWarning("Empty idToken received");
|
||||
return BadRequest("IdToken is required");
|
||||
}
|
||||
|
||||
var (isValid, user, error) = await googleAuthService.ValidateGoogleTokenAsync(idToken);
|
||||
|
||||
if (!isValid || user == null)
|
||||
{
|
||||
logger.LogWarning("Google token validation failed: {Error}", error);
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var jwt = jwtTokenService.GenerateToken(user);
|
||||
|
||||
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.Reflection;
|
||||
using System.Text;
|
||||
using DiunaBI.API.Services;
|
||||
using DiunaBI.Infrastructure.Data;
|
||||
using DiunaBI.Infrastructure.Services;
|
||||
using Google.Apis.Sheets.v4;
|
||||
@@ -74,6 +75,9 @@ builder.Services.AddAuthentication(options =>
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<GoogleAuthService>();
|
||||
builder.Services.AddScoped<JwtTokenService>();
|
||||
|
||||
// Google Sheets dependencies
|
||||
Console.WriteLine("Adding Google Sheets dependencies...");
|
||||
builder.Services.AddSingleton<GoogleSheetsHelper>();
|
||||
@@ -159,6 +163,9 @@ app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "OK", timestamp = DateTime.UtcNow }))
|
||||
.AllowAnonymous();
|
||||
|
||||
app.Run();
|
||||
|
||||
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}#"
|
||||
},
|
||||
"InstanceName": "#{app-environment}#",
|
||||
"GoogleClientId": "#{google-backend-login-client-id}#",
|
||||
"GoogleAuth": {
|
||||
"ClientId": "#{google-backend-login-client-id}#"
|
||||
},
|
||||
"Secret": "#{google-backend-login-secret}#",
|
||||
"apiKey": "#{api-key}#",
|
||||
"powerBI-user": "#{powerBI-user}#",
|
||||
|
||||
@@ -8,7 +8,7 @@ public class User
|
||||
#region Properties
|
||||
public Guid Id { get; init; }
|
||||
public string? Email { get; init; }
|
||||
public string? UserName { get; init; }
|
||||
public string? UserName { get; set; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,44 @@
|
||||
@page "/dashboard"
|
||||
@using DiunaBI.UI.Shared.Services
|
||||
@using MudBlazor
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<MudText Typo="Typo.h4">Dashboard</MudText>
|
||||
<p>Tutaj znajdzie się panel ogólny aplikacji</p>
|
||||
<MudText Typo="Typo.body1" Class="mt-4">Witaj w DiunaBI!</MudText>
|
||||
|
||||
@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">
|
||||
<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">
|
||||
Zaloguj się używając konta Google
|
||||
</MudText>
|
||||
@@ -22,6 +22,7 @@
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
||||
<span>Weryfikacja...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4" Dense="true">
|
||||
@_errorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
@@ -94,27 +95,41 @@
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var userInfo = new UserInfo
|
||||
try
|
||||
{
|
||||
FullName = fullName,
|
||||
Email = email,
|
||||
AvatarUrl = avatarUrl
|
||||
};
|
||||
|
||||
await _instance.AuthService.SetAuthenticationAsync(accessToken, userInfo);
|
||||
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = string.Empty;
|
||||
|
||||
_instance.NavigationManager.NavigateTo("/dashboard", replace: true);
|
||||
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
// Waliduj użytkownika w backendzie DiunaBI
|
||||
var (success, errorMessage) = await _instance.AuthService.ValidateWithBackendAsync(
|
||||
googleCredential, fullName, email, avatarUrl);
|
||||
|
||||
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ć.";
|
||||
}
|
||||
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"❌ OnGoogleSignInSuccess error: {ex.Message}");
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = "Błąd podczas weryfikacji użytkownika.";
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
OnClick="ToggleDrawer"
|
||||
Class="mud-hidden-md-up"/>
|
||||
<MudSpacer/>
|
||||
<MudText Typo="Typo.h6">BimAI</MudText>
|
||||
<MudText Typo="Typo.h6">DiunaBI</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-Open="_drawerOpen"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
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/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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -5,90 +6,153 @@ namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
public class UserInfo
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string AvatarUrl { get; set; } = string.Empty;
|
||||
}
|
||||
public class AuthService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private bool? _isAuthenticated;
|
||||
private UserInfo? _userInfo = null;
|
||||
private string? _apiToken;
|
||||
|
||||
public event Action<bool>? AuthenticationStateChanged;
|
||||
|
||||
public AuthService(IJSRuntime jsRuntime)
|
||||
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public bool IsAuthenticated => _isAuthenticated ?? false;
|
||||
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()
|
||||
{
|
||||
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");
|
||||
|
||||
_isAuthenticated = !string.IsNullOrEmpty(token);
|
||||
|
||||
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
|
||||
{
|
||||
_apiToken = token;
|
||||
_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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"AuthService.CheckAuthentication ERROR: {ex.Message}");
|
||||
Console.WriteLine($"❌ CheckAuthentication ERROR: {ex.Message}");
|
||||
_isAuthenticated = false;
|
||||
_userInfo = null;
|
||||
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()
|
||||
{
|
||||
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");
|
||||
|
||||
_apiToken = null;
|
||||
_isAuthenticated = false;
|
||||
_userInfo = null;
|
||||
Console.WriteLine($"AuthService.ClearAuthentication: token and user ingfo removed");
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
|
||||
Console.WriteLine("✅ Authentication cleared");
|
||||
AuthenticationStateChanged?.Invoke(false);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
if (_isAuthenticated != true) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _apiToken;
|
||||
}
|
||||
|
||||
private class ApiTokenResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public Guid Id { get; set; }
|
||||
public DateTime ExpirationTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,98 @@
|
||||
let googleClient = null;
|
||||
let isSigningIn = false;
|
||||
let googleInitialized = false;
|
||||
|
||||
function waitForGoogleApi() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.google?.accounts?.oauth2) {
|
||||
resolve(window.google);
|
||||
return;
|
||||
}
|
||||
|
||||
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 = '') {
|
||||
const errorMessage = error?.message || error?.type || error?.toString() || 'Unknown error';
|
||||
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;
|
||||
window.initGoogleSignIn = function(clientId) {
|
||||
if (googleInitialized) {
|
||||
console.log("Google Sign-In already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const google = await waitForGoogleApi();
|
||||
|
||||
googleClient = google.accounts.oauth2.initTokenClient({
|
||||
client_id: clientId,
|
||||
scope: 'email profile',
|
||||
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 (!userInfo) return;
|
||||
|
||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInSuccess',
|
||||
tokenResponse.access_token,
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error('Initiaxcrun xctrace list deviceslization error:', error);
|
||||
await DotNet.invokeMethodAsync('BimAI.UI.Shared', 'OnGoogleSignInError',
|
||||
error.message || 'Failed to initialize Google Sign-In');
|
||||
isSigningIn = false;
|
||||
}
|
||||
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 = async function() {
|
||||
if (isSigningIn) {
|
||||
console.log('Sign-in already in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
window.requestGoogleSignIn = function() {
|
||||
console.log("🚀 Requesting Google Sign-In...");
|
||||
|
||||
isSigningIn = true;
|
||||
googleClient.requestAccessToken();
|
||||
};
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function handleCredentialResponse(response) {
|
||||
console.log("=== 🎉 Google Credential Response ===");
|
||||
|
||||
try {
|
||||
if (!response.credential) {
|
||||
throw new Error("No credential in response");
|
||||
}
|
||||
|
||||
const tokenParts = response.credential.split('.');
|
||||
console.log("📝 ID Token parts:", tokenParts.length); // Should be 3 (JWT)
|
||||
console.log("📏 ID Token length:", response.credential.length);
|
||||
|
||||
if (tokenParts.length !== 3) {
|
||||
throw new Error("Invalid JWT format - expected 3 parts (header.payload.signature)");
|
||||
}
|
||||
|
||||
// Dekoduj payload JWT aby wyciągnąć user info
|
||||
const payload = decodeJwtPayload(response.credential);
|
||||
|
||||
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) {
|
||||
console.error("❌ Error processing Google credential:", error);
|
||||
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInError', error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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/icon?family=Material+Icons" 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>
|
||||
<HeadOutlet />
|
||||
</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