App login is working
All checks were successful
Build Docker Images / test (push) Successful in 1m37s
Build Docker Images / build-and-push (push) Successful in 1m52s

This commit is contained in:
Michał Zieliński
2025-11-09 19:39:52 +01:00
parent 95438efcbd
commit f7b9009215
14 changed files with 466 additions and 227 deletions

View File

@@ -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");
}
}
}

View File

@@ -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())

View 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");
}
}
}

View 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;
}
}
}

View File

@@ -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}#",

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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());
}
}
}

View File

@@ -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"

View File

@@ -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;

View File

@@ -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; }
}
}

View File

@@ -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");
}
}

View File

@@ -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>

View File

@@ -1,2 +1,2 @@
###
GET http://localhost:5400/api/Layers/ProcessQueue/10763478CB738D4ecb2h76g803478CB738D4e
GET http://localhost:5400/health