after refactor cleanup
This commit is contained in:
42
DiunaBI.UI.Shared/Components/AuthGuard.razor
Normal file
42
DiunaBI.UI.Shared/Components/AuthGuard.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@using DiunaBI.UI.Shared.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="d-flex justify-center align-center" style="height: 100vh;">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
|
||||
</div>
|
||||
}
|
||||
else if (_isAuthenticated)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private bool _isLoading = true;
|
||||
private bool _isAuthenticated = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Console.WriteLine("AuthGuard: Checking authentication...");
|
||||
|
||||
_isAuthenticated = await AuthService.CheckAuthenticationAsync();
|
||||
_isLoading = false;
|
||||
|
||||
Console.WriteLine($"AuthGuard: isAuthenticated={_isAuthenticated}");
|
||||
|
||||
if (!_isAuthenticated)
|
||||
{
|
||||
Console.WriteLine("AuthGuard: Redirecting to /login");
|
||||
Navigation.NavigateTo("/login", replace: true);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
DiunaBI.UI.Shared/Components/Dashboard.razor
Normal file
42
DiunaBI.UI.Shared/Components/Dashboard.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@page "/dashboard"
|
||||
@using DiunaBI.UI.Shared.Services
|
||||
@using MudBlazor
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@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">
|
||||
✅ Signed in via Google
|
||||
</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mt-4">
|
||||
You are not logged in
|
||||
</MudAlert>
|
||||
}
|
||||
15
DiunaBI.UI.Shared/Components/Index.razor
Normal file
15
DiunaBI.UI.Shared/Components/Index.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@page "/"
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code
|
||||
{
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Navigation.NavigateTo("/dashboard");
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
}
|
||||
72
DiunaBI.UI.Shared/Components/LayerListComponent.razor
Normal file
72
DiunaBI.UI.Shared/Components/LayerListComponent.razor
Normal file
@@ -0,0 +1,72 @@
|
||||
@using MudBlazor.Internal
|
||||
<MudExpansionPanels Class="mb-4">
|
||||
<MudExpansionPanel Icon="@Icons.Material.Filled.FilterList"
|
||||
Text="Filters"
|
||||
Expanded="true">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudTextField @bind-Value="filterRequest.Search"
|
||||
Label="Search"
|
||||
Placeholder="Name, number..."
|
||||
Immediate="true"
|
||||
DebounceInterval="500"
|
||||
OnDebounceIntervalElapsed="SearchLayers"
|
||||
Clearable="true"/>
|
||||
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
OnClick="ClearFilters"
|
||||
StartIcon="Icons.Material.Filled.Clear">
|
||||
Clear filters
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
|
||||
<MudDivider Class="my-4"></MudDivider>
|
||||
|
||||
<MudTable Items="layers.Items"
|
||||
Dense="true"
|
||||
Hover="true"
|
||||
Loading="isLoading"
|
||||
LoadingProgressColor="Color.Info"
|
||||
OnRowClick="@((TableRowClickEventArgs<LayerDto> args) => OnRowClick(args.Item))"
|
||||
T="LayerDto"
|
||||
Style="cursor: pointer;">
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Type</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Name">@context.Name</MudTd>
|
||||
<MudTd DataLabel="Type">@context.Type</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>No layers to display</MudText>
|
||||
</NoRecordsContent>
|
||||
<LoadingContent>
|
||||
Loading...
|
||||
</LoadingContent>
|
||||
</MudTable>
|
||||
|
||||
@if (layers.TotalCount > 0)
|
||||
{
|
||||
<MudGrid Class="mt-4" AlignItems="Center.Center">
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudText Typo="Typo.body2">
|
||||
Results @((layers.Page - 1) * layers.PageSize + 1) - @Math.Min(layers.Page * layers.PageSize, layers.TotalCount)
|
||||
of @layers.TotalCount
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" Class="d-flex justify-end">
|
||||
<MudPagination Count="layers.TotalPages"
|
||||
Selected="layers.Page"
|
||||
SelectedChanged="OnPageChanged"
|
||||
ShowFirstButton="true"
|
||||
ShowLastButton="true"/>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
65
DiunaBI.UI.Shared/Components/LayerListComponent.razor.cs
Normal file
65
DiunaBI.UI.Shared/Components/LayerListComponent.razor.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using DiunaBI.UI.Shared.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DiunaBI.Application.DTOModels;
|
||||
using DiunaBI.Application.DTOModels.Common;
|
||||
using MudBlazor;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Components;
|
||||
|
||||
public partial class LayerListComponent : ComponentBase
|
||||
{
|
||||
[Inject] private LayerService LayerService { get; set; } = default!;
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
|
||||
private PagedResult<LayerDto> layers = new();
|
||||
private LayerFilterRequest filterRequest = new();
|
||||
private bool isLoading = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadLayers();
|
||||
}
|
||||
|
||||
private async Task LoadLayers()
|
||||
{
|
||||
isLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
layers = await LayerService.GetLayersAsync(filterRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Loading layers failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SearchLayers()
|
||||
{
|
||||
filterRequest.Page = 1;
|
||||
await LoadLayers();
|
||||
}
|
||||
|
||||
private async Task OnPageChanged(int page)
|
||||
{
|
||||
filterRequest.Page = page;
|
||||
await LoadLayers();
|
||||
}
|
||||
|
||||
private async Task ClearFilters()
|
||||
{
|
||||
filterRequest = new LayerFilterRequest();
|
||||
await LoadLayers();
|
||||
}
|
||||
|
||||
private void OnRowClick(LayerDto layer)
|
||||
{
|
||||
NavigationManager.NavigateTo($"/layers/{layer.Id}");
|
||||
}
|
||||
}
|
||||
200
DiunaBI.UI.Shared/Components/LoginCard.razor
Normal file
200
DiunaBI.UI.Shared/Components/LoginCard.razor
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
@using DiunaBI.UI.Shared.Services
|
||||
@using Microsoft.Extensions.Configuration
|
||||
@inject IJSRuntime JS
|
||||
@inject IConfiguration Configuration
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IGoogleAuthService? GoogleAuthService
|
||||
|
||||
<MudCard Class="login-card" Elevation="8">
|
||||
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Welcome to DiunaBI</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-6 text-center">
|
||||
Sign in using your Google account
|
||||
</MudText>
|
||||
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Custom.Brands.Google"
|
||||
Size="Size.Large"
|
||||
OnClick="HandleGoogleSignIn"
|
||||
Disabled="@_isLoading">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
||||
<span>Verifying...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Sign in with Google</span>
|
||||
}
|
||||
</MudButton>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4" Dense="true">
|
||||
@_errorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
@code {
|
||||
private bool _isLoading = false;
|
||||
private string _errorMessage = string.Empty;
|
||||
private static LoginCard? _instance;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_instance = this;
|
||||
|
||||
// Initialize JavaScript Google SDK for web (both null service and WebGoogleAuthService)
|
||||
// Skip only for mobile platform-specific auth (MobileGoogleAuthService)
|
||||
var isMobileAuth = GoogleAuthService != null && GoogleAuthService.GetType().Name == "MobileGoogleAuthService";
|
||||
|
||||
if (!isMobileAuth)
|
||||
{
|
||||
await InitializeGoogleSignIn();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("📱 Using platform-specific Google auth service");
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeGoogleSignIn()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
var clientId = Configuration["GoogleAuth:ClientId"];
|
||||
Console.WriteLine($"🔍 Reading GoogleAuth:ClientId from configuration: '{clientId}'");
|
||||
|
||||
if (string.IsNullOrEmpty(clientId))
|
||||
{
|
||||
_errorMessage = "Google ClientId is not configured in appsettings.";
|
||||
Console.Error.WriteLine("❌ Google ClientId is NULL or EMPTY in configuration!");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"✅ Calling initGoogleSignIn with clientId: {clientId}");
|
||||
await JS.InvokeVoidAsync("initGoogleSignIn", clientId);
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = "Google Sign-In initialization error.";
|
||||
Console.Error.WriteLine($"❌ Google Sign-In initialization error: {ex.Message}");
|
||||
Console.Error.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleGoogleSignIn()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
// Use platform-specific auth if available (mobile), otherwise use JavaScript (web)
|
||||
if (GoogleAuthService != null)
|
||||
{
|
||||
Console.WriteLine("📱 Using mobile Google auth");
|
||||
var success = await GoogleAuthService.SignInAsync();
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("✅ Mobile auth successful, navigating...");
|
||||
NavigationManager.NavigateTo("/dashboard", replace: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorMessage = "Login failed. Please try again";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("🌐 Using web JavaScript Google auth");
|
||||
await JS.InvokeVoidAsync("requestGoogleSignIn");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"❌ HandleGoogleSignIn error: {ex.Message}");
|
||||
_errorMessage = "Login error. Please try again";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public static async Task OnGoogleSignInSuccess(string googleCredential, string fullName, string email, string avatarUrl)
|
||||
{
|
||||
Console.WriteLine($"=== OnGoogleSignInSuccess: {email} ===");
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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 ?? "Login failed.";
|
||||
}
|
||||
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"❌ OnGoogleSignInSuccess error: {ex.Message}");
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = "User verification error.";
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public static async Task OnGoogleSignInError(string error)
|
||||
{
|
||||
Console.WriteLine($"Google SignIn Error: {error}");
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
_instance._isLoading = false;
|
||||
_instance._errorMessage = "Google login error. Please try again";
|
||||
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
10
DiunaBI.UI.Shared/Components/NavMenu.razor
Normal file
10
DiunaBI.UI.Shared/Components/NavMenu.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using MudBlazor
|
||||
<MudNavMenu>
|
||||
<MudNavLink href="dashboard" Icon="@Icons.Material.Filled.Dashboard" Match="NavLinkMatch.All">
|
||||
Dashboard
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="layers" Icon="@Icons.Material.Filled.List" Match="NavLinkMatch.All">
|
||||
Layers
|
||||
</MudNavLink>
|
||||
</MudNavMenu>
|
||||
18
DiunaBI.UI.Shared/Components/Routes.razor
Normal file
18
DiunaBI.UI.Shared/Components/Routes.razor
Normal file
@@ -0,0 +1,18 @@
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using MudBlazor
|
||||
|
||||
<Router AppAssembly="@typeof(Routes).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<MudCard Elevation="0">
|
||||
<MudText Typo="Typo.h6">
|
||||
Strona nieznaleziona.
|
||||
</MudText>
|
||||
</MudCard>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
30
DiunaBI.UI.Shared/DiunaBI.UI.Shared.csproj
Normal file
30
DiunaBI.UI.Shared/DiunaBI.UI.Shared.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0"/>
|
||||
<PackageReference Include="MudBlazor" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiunaBI.Application\DiunaBI.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Interfaces\" />
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
DiunaBI.UI.Shared/EmptyLayout.razor
Normal file
8
DiunaBI.UI.Shared/EmptyLayout.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
|
||||
@Body
|
||||
42
DiunaBI.UI.Shared/Extensions/ServiceCollectionExtensions.cs
Normal file
42
DiunaBI.UI.Shared/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DiunaBI.UI.Shared.Services;
|
||||
using DiunaBI.UI.Shared.Handlers;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSharedServices(this IServiceCollection services, string apiBaseUrl)
|
||||
{
|
||||
// HttpClient for API calls with logging
|
||||
// Ensure BaseAddress ends with / for proper relative URL resolution
|
||||
var baseUri = apiBaseUrl.EndsWith('/') ? apiBaseUrl : apiBaseUrl + "/";
|
||||
|
||||
Console.WriteLine($"🔧 Configuring HttpClient with BaseAddress: {baseUri}");
|
||||
|
||||
services.AddTransient<HttpLoggingHandler>();
|
||||
|
||||
// Configure named HttpClient with logging handler
|
||||
services.AddHttpClient("DiunaBI", client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(baseUri);
|
||||
Console.WriteLine($"✅ HttpClient BaseAddress set to: {client.BaseAddress}");
|
||||
})
|
||||
.AddHttpMessageHandler<HttpLoggingHandler>();
|
||||
|
||||
// Register a scoped HttpClient factory that services will use
|
||||
services.AddScoped<HttpClient>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IHttpClientFactory>();
|
||||
var client = factory.CreateClient("DiunaBI");
|
||||
Console.WriteLine($"🏭 HttpClient created from factory. BaseAddress: {client.BaseAddress}");
|
||||
return client;
|
||||
});
|
||||
|
||||
// Services
|
||||
services.AddScoped<AuthService>();
|
||||
services.AddScoped<LayerService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
28
DiunaBI.UI.Shared/Handlers/HttpLoggingHandler.cs
Normal file
28
DiunaBI.UI.Shared/Handlers/HttpLoggingHandler.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace DiunaBI.UI.Shared.Handlers;
|
||||
|
||||
public class HttpLoggingHandler : DelegatingHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine($"🌐 HTTP Request: {request.Method} {request.RequestUri}");
|
||||
Console.WriteLine($" BaseAddress: {request.RequestUri?.GetLeftPart(UriPartial.Authority)}");
|
||||
Console.WriteLine($" Path: {request.RequestUri?.PathAndQuery}");
|
||||
|
||||
if (request.Headers.Authorization != null)
|
||||
{
|
||||
Console.WriteLine($" Auth: {request.Headers.Authorization.Scheme} {request.Headers.Authorization.Parameter?[..Math.Min(20, request.Headers.Authorization.Parameter?.Length ?? 0)]}...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" Auth: None");
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
Console.WriteLine($" Response: {(int)response.StatusCode} {response.StatusCode}");
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
81
DiunaBI.UI.Shared/MainLayout.razor
Normal file
81
DiunaBI.UI.Shared/MainLayout.razor
Normal file
@@ -0,0 +1,81 @@
|
||||
@using MudBlazor
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AuthGuard>
|
||||
<MudThemeProvider Theme="_theme"/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
<MudLayout>
|
||||
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
||||
<MudAppBar Elevation="0">
|
||||
<MudIconButton
|
||||
Icon="@Icons.Material.Filled.Menu"
|
||||
Color="Color.Inherit"
|
||||
Edge="Edge.Start"
|
||||
OnClick="ToggleDrawer"
|
||||
Class="mud-hidden-md-up"/>
|
||||
<MudSpacer/>
|
||||
<MudText Typo="Typo.h6">DiunaBI</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-Open="_drawerOpen"
|
||||
Anchor="Anchor.Start"
|
||||
Variant="@_drawerVariant"
|
||||
Elevation="1"
|
||||
ClipMode="DrawerClipMode.Always"
|
||||
Class="mud-width-250">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||
<MudNavLink Href="/layers" Icon="@Icons.Material.Filled.Inventory">Layers</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
</AuthGuard>
|
||||
|
||||
@code {
|
||||
|
||||
private bool _drawerOpen = true;
|
||||
private DrawerVariant _drawerVariant = DrawerVariant.Persistent;
|
||||
|
||||
private MudTheme _theme = new MudTheme()
|
||||
{
|
||||
PaletteLight = new PaletteLight()
|
||||
{
|
||||
Primary = "#e7163d",
|
||||
PrimaryDarken = "#c01234",
|
||||
PrimaryLighten = "#f04366",
|
||||
Secondary = "#424242",
|
||||
AppbarBackground = "#e7163d",
|
||||
}
|
||||
};
|
||||
|
||||
void ToggleDrawer()
|
||||
{
|
||||
Console.WriteLine($"ToogleDrawer clickkk {DateTime.Now}");
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
|
||||
private void OnBreakpointChanged(Breakpoint breakpoint)
|
||||
{
|
||||
if (breakpoint < Breakpoint.Md)
|
||||
{
|
||||
_drawerVariant = DrawerVariant.Temporary;
|
||||
_drawerOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_drawerVariant = DrawerVariant.Persistent;
|
||||
_drawerOpen = true;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
112
DiunaBI.UI.Shared/Pages/LayerDetailPage.razor
Normal file
112
DiunaBI.UI.Shared/Pages/LayerDetailPage.razor
Normal file
@@ -0,0 +1,112 @@
|
||||
@page "/layers/{id:guid}"
|
||||
@using DiunaBI.UI.Shared.Services
|
||||
@using DiunaBI.Application.DTOModels
|
||||
@using MudBlazor
|
||||
@inject LayerService LayerService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h5">Layer Details</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudButton Variant="Variant.Text" OnClick="Export">Export</MudButton>
|
||||
@if (layer != null && layer.Type == LayerType.Administration)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Href="@($"/layers/edit/{layer.Id}/duplicate")">Duplicate</MudButton>
|
||||
<MudButton Variant="Variant.Text" Href="@($"/layers/edit/{layer.Id}")">Edit</MudButton>
|
||||
}
|
||||
@if (layer != null && layer.Type == LayerType.Processed)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" OnClick="ProcessLayer">Process Layer</MudButton>
|
||||
}
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (layer == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">Layer not found</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="layer.Name"
|
||||
Label="Name"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="true"
|
||||
FullWidth="true"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
@if (layer.IsCancelled)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true">
|
||||
This layer is cancelled. Will not be used in any further processing.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField Value="@layer.CreatedAt.ToString("g")"
|
||||
Label="Created"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="true"
|
||||
FullWidth="true"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentText="@(layer.CreatedBy?.Username ?? "")"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField Value="@layer.ModifiedAt.ToString("g")"
|
||||
Label="Modified"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="true"
|
||||
FullWidth="true"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentText="@(layer.ModifiedBy?.Username ?? "")"/>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDivider Class="my-4"/>
|
||||
|
||||
<MudTable Items="@records"
|
||||
Dense="true"
|
||||
Striped="true"
|
||||
FixedHeader="true"
|
||||
FixedFooter="true"
|
||||
Height="600px">
|
||||
<HeaderContent>
|
||||
<MudTh>Code</MudTh>
|
||||
@foreach (var column in displayedColumns)
|
||||
{
|
||||
<MudTh>@column</MudTh>
|
||||
}
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Code">@context.Code</MudTd>
|
||||
@foreach (var column in displayedColumns)
|
||||
{
|
||||
<MudTd DataLabel="@column">@GetRecordValue(context, column)</MudTd>
|
||||
}
|
||||
</RowTemplate>
|
||||
<FooterContent>
|
||||
<MudTd><b>Value1 sum</b></MudTd>
|
||||
@foreach (var column in displayedColumns)
|
||||
{
|
||||
@if (column == "Value1")
|
||||
{
|
||||
<MudTd><b>@valueSum.ToString("N2")</b></MudTd>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTd></MudTd>
|
||||
}
|
||||
}
|
||||
</FooterContent>
|
||||
</MudTable>
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
122
DiunaBI.UI.Shared/Pages/LayerDetailPage.razor.cs
Normal file
122
DiunaBI.UI.Shared/Pages/LayerDetailPage.razor.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using DiunaBI.Application.DTOModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Pages;
|
||||
|
||||
public partial class LayerDetailPage : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; set; } = null!;
|
||||
|
||||
private LayerDto? layer;
|
||||
private List<RecordDto> records = new();
|
||||
private List<string> displayedColumns = new();
|
||||
private double valueSum = 0;
|
||||
private bool isLoading = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadLayer();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadLayer();
|
||||
}
|
||||
|
||||
private async Task LoadLayer()
|
||||
{
|
||||
isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
layer = await LayerService.GetLayerByIdAsync(Id);
|
||||
|
||||
if (layer != null && layer.Records != null)
|
||||
{
|
||||
records = layer.Records;
|
||||
CalculateDisplayedColumns();
|
||||
CalculateValueSum();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error loading layer: {ex.Message}");
|
||||
Snackbar.Add("Error loading layer", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateDisplayedColumns()
|
||||
{
|
||||
displayedColumns.Clear();
|
||||
|
||||
// Check which Value columns have data
|
||||
for (int i = 1; i <= 32; i++)
|
||||
{
|
||||
var columnName = $"Value{i}";
|
||||
var hasData = records.Any(r => GetRecordValueByName(r, columnName) != null);
|
||||
|
||||
if (hasData)
|
||||
{
|
||||
displayedColumns.Add(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Desc1 has data
|
||||
if (records.Any(r => !string.IsNullOrEmpty(r.Desc1)))
|
||||
{
|
||||
displayedColumns.Add("Description1");
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateValueSum()
|
||||
{
|
||||
valueSum = records
|
||||
.Where(r => r.Value1.HasValue)
|
||||
.Sum(r => r.Value1!.Value);
|
||||
}
|
||||
|
||||
private string GetRecordValue(RecordDto record, string columnName)
|
||||
{
|
||||
if (columnName == "Description1")
|
||||
{
|
||||
return record.Desc1 ?? string.Empty;
|
||||
}
|
||||
|
||||
var value = GetRecordValueByName(record, columnName);
|
||||
return value.HasValue ? value.Value.ToString("N2") : string.Empty;
|
||||
}
|
||||
|
||||
private double? GetRecordValueByName(RecordDto record, string columnName)
|
||||
{
|
||||
var property = typeof(RecordDto).GetProperty(columnName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (property != null && property.PropertyType == typeof(double?))
|
||||
{
|
||||
return property.GetValue(record) as double?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Export()
|
||||
{
|
||||
// TODO: Implement export functionality
|
||||
Snackbar.Add("Export functionality coming soon", Severity.Error);
|
||||
}
|
||||
|
||||
private void ProcessLayer()
|
||||
{
|
||||
// TODO: Implement process layer functionality
|
||||
Snackbar.Add("Process layer functionality coming soon", Severity.Error);
|
||||
}
|
||||
}
|
||||
8
DiunaBI.UI.Shared/Pages/LayerListPage.razor
Normal file
8
DiunaBI.UI.Shared/Pages/LayerListPage.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/layers"
|
||||
@using DiunaBI.UI.Shared.Components
|
||||
|
||||
<PageTitle>Layers</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
|
||||
<LayerListComponent />
|
||||
</MudContainer>
|
||||
46
DiunaBI.UI.Shared/Pages/LoginPage.razor
Normal file
46
DiunaBI.UI.Shared/Pages/LoginPage.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
@page "/login"
|
||||
@layout EmptyLayout
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<LoginCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/DiunaBI.UI.Shared/images/login-background.jpg') no-repeat center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
</style>
|
||||
172
DiunaBI.UI.Shared/Services/AuthService.cs
Normal file
172
DiunaBI.UI.Shared/Services/AuthService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
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(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} ===");
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync("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
|
||||
};
|
||||
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "api_token", _apiToken);
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", JsonSerializer.Serialize(_userInfo));
|
||||
|
||||
_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, "User does not exist in DiunaBI database.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"❌ Backend error: {response.StatusCode}");
|
||||
return (false, "DiunaBI server error. Please try again.");
|
||||
}
|
||||
|
||||
return (false, "Unexpected error.");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Network error: {ex.Message}");
|
||||
return (false, "Cannot connect to DiunaBI server.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Validation error: {ex.Message}");
|
||||
return (false, "User verification error.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CheckAuthenticationAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
// Restore 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.CheckAuthenticationAsync END (authenticated={_isAuthenticated}) ===");
|
||||
|
||||
return _isAuthenticated.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ CheckAuthentication ERROR: {ex.Message}");
|
||||
_isAuthenticated = false;
|
||||
_userInfo = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearAuthenticationAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("=== AuthService.ClearAuthenticationAsync ===");
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "api_token");
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
||||
|
||||
_apiToken = null;
|
||||
_isAuthenticated = false;
|
||||
_userInfo = null;
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
|
||||
Console.WriteLine("✅ Authentication cleared");
|
||||
AuthenticationStateChanged?.Invoke(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ ClearAuthentication ERROR: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetTokenAsync()
|
||||
{
|
||||
if (_isAuthenticated != true)
|
||||
{
|
||||
await CheckAuthenticationAsync();
|
||||
}
|
||||
|
||||
return _apiToken;
|
||||
}
|
||||
|
||||
private class ApiTokenResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public Guid Id { get; set; }
|
||||
public DateTime ExpirationTime { get; set; }
|
||||
}
|
||||
}
|
||||
6
DiunaBI.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
6
DiunaBI.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
public class GoogleAuthConfig
|
||||
{
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
}
|
||||
13
DiunaBI.UI.Shared/Services/IGoogleAuthService.cs
Normal file
13
DiunaBI.UI.Shared/Services/IGoogleAuthService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-agnostic interface for Google authentication
|
||||
/// </summary>
|
||||
public interface IGoogleAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiate Google Sign-In flow
|
||||
/// </summary>
|
||||
/// <returns>True if authentication was initiated successfully</returns>
|
||||
Task<bool> SignInAsync();
|
||||
}
|
||||
61
DiunaBI.UI.Shared/Services/LayerService.cs
Normal file
61
DiunaBI.UI.Shared/Services/LayerService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DiunaBI.Application.DTOModels;
|
||||
using DiunaBI.Application.DTOModels.Common;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
public class LayerService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public LayerService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public async Task<PagedResult<LayerDto>> GetLayersAsync(LayerFilterRequest filterRequest)
|
||||
{
|
||||
// Calculate start index from page number (page 1 = start 0, page 2 = start 50, etc.)
|
||||
var start = (filterRequest.Page - 1) * filterRequest.PageSize;
|
||||
var query = $"Layers?start={start}&limit={filterRequest.PageSize}";
|
||||
|
||||
if (!string.IsNullOrEmpty(filterRequest.Search))
|
||||
query += $"&name={Uri.EscapeDataString(filterRequest.Search)}";
|
||||
|
||||
/*
|
||||
if (type.HasValue)
|
||||
query += $"&type={type.Value}";
|
||||
*/
|
||||
|
||||
var response = await _httpClient.GetAsync(query);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<PagedResult<LayerDto>>(json, _jsonOptions);
|
||||
|
||||
return result ?? new PagedResult<LayerDto>();
|
||||
}
|
||||
|
||||
public async Task<LayerDto?> GetLayerByIdAsync(Guid id)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"Layers/{id}");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<LayerDto>();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateRecordsAsync(Guid layerId, List<RecordDto> records)
|
||||
{
|
||||
// TODO: Implement if needed - backend doesn't have PUT endpoint yet
|
||||
// For now we don't need it for read-only view
|
||||
return await Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
30
DiunaBI.UI.Shared/Services/WebGoogleAuthService.cs
Normal file
30
DiunaBI.UI.Shared/Services/WebGoogleAuthService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Web implementation of Google authentication using JavaScript SDK
|
||||
/// </summary>
|
||||
public class WebGoogleAuthService : IGoogleAuthService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
public WebGoogleAuthService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<bool> SignInAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("requestGoogleSignIn");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Web Google Sign-In error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
DiunaBI.UI.Shared/_Imports.razor
Normal file
12
DiunaBI.UI.Shared/_Imports.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using DiunaBI.UI.Shared
|
||||
@using DiunaBI.UI.Shared.Components
|
||||
@using DiunaBI.Application.DTOModels
|
||||
@using MudBlazor
|
||||
BIN
DiunaBI.UI.Shared/wwwroot/images/login-background.jpg
Normal file
BIN
DiunaBI.UI.Shared/wwwroot/images/login-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 965 KiB |
102
DiunaBI.UI.Shared/wwwroot/js/auth.js
Normal file
102
DiunaBI.UI.Shared/wwwroot/js/auth.js
Normal file
@@ -0,0 +1,102 @@
|
||||
let googleInitialized = false;
|
||||
|
||||
window.initGoogleSignIn = function(clientId) {
|
||||
if (googleInitialized) {
|
||||
console.log("Google Sign-In already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔐 Initializing Google Sign-In (ID Token flow)");
|
||||
console.log("📋 Received clientId:", clientId);
|
||||
console.log("📋 ClientId type:", typeof clientId);
|
||||
console.log("📋 ClientId length:", clientId ? clientId.length : 0);
|
||||
|
||||
if (!clientId || clientId === '' || clientId === 'null' || clientId === 'undefined') {
|
||||
console.error("❌ Invalid clientId received:", clientId);
|
||||
throw new Error("ClientId is null, empty, or invalid");
|
||||
}
|
||||
|
||||
// Check if Google library is loaded
|
||||
if (typeof google === 'undefined' || !google.accounts || !google.accounts.id) {
|
||||
console.error("❌ Google Sign-In library not loaded yet!");
|
||||
throw new Error("Google Sign-In library not ready");
|
||||
}
|
||||
|
||||
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...");
|
||||
|
||||
google.accounts.id.prompt();
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user