Implement Google authentication (for Web) and user management system
This commit is contained in:
@@ -7,16 +7,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Apis.Auth" Version="1.70.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.17" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.17" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.17">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.17">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Bimix.Domain\Bimix.Domain.csproj" />
|
|
||||||
<ProjectReference Include="..\Bimix.Application\Bimix.Application.csproj" />
|
<ProjectReference Include="..\Bimix.Application\Bimix.Application.csproj" />
|
||||||
<ProjectReference Include="..\Bimix.Infrastructure\Bimix.Infrastructure.csproj" />
|
<ProjectReference Include="..\Bimix.Infrastructure\Bimix.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
96
Bimix.API/Controllers/AuthController.cs
Normal file
96
Bimix.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Bimix.API.Services;
|
||||||
|
using Bimix.Application.DTOModels;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bimix.API.Controllers;
|
||||||
|
|
||||||
|
public class AuthController(
|
||||||
|
GoogleAuthService googleAuthService,
|
||||||
|
JwtTokenService jwtTokenService,
|
||||||
|
ILogger<AuthController> logger)
|
||||||
|
: ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost("google")]
|
||||||
|
public async Task<IActionResult> GoogleAuth([FromBody] GoogleAuthRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.IdToken))
|
||||||
|
{
|
||||||
|
return BadRequest(new GoogleAuthResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = "IdToken is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var (isValid, user, error) = await googleAuthService.ValidateGoogleTokenAsync(request.IdToken);
|
||||||
|
|
||||||
|
if (!isValid || user == null)
|
||||||
|
{
|
||||||
|
var statusCode = error switch
|
||||||
|
{
|
||||||
|
"User not authorized to access this application" => 403,
|
||||||
|
"User account is not active" => 403,
|
||||||
|
"Invalid Google token" => 401,
|
||||||
|
_ => 401
|
||||||
|
};
|
||||||
|
|
||||||
|
return StatusCode(statusCode, new GoogleAuthResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = error ?? "Authentication failed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwt = jwtTokenService.GenerateToken(user);
|
||||||
|
|
||||||
|
return Ok(new GoogleAuthResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Token = jwt,
|
||||||
|
User = new UserDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Email = user.Email,
|
||||||
|
FullName = user.FullName,
|
||||||
|
IsActive = user.IsActive,
|
||||||
|
LastLoginAt = user.LastLoginAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error during Google authentication");
|
||||||
|
return StatusCode(500, new GoogleAuthResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = "Internal server error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("me")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult GetCurrentUser()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
|
||||||
|
var emailClaim = User.FindFirst(ClaimTypes.Email);
|
||||||
|
var nameClaim = User.FindFirst(ClaimTypes.Name);
|
||||||
|
|
||||||
|
if (userIdClaim == null || emailClaim == null || nameClaim == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new UserDto
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(userIdClaim.Value),
|
||||||
|
Email = emailClaim.Value,
|
||||||
|
FullName = nameClaim.Value,
|
||||||
|
IsActive = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Bimix.API.Services;
|
||||||
using Bimix.Infrastructure.Data;
|
using Bimix.Infrastructure.Data;
|
||||||
using Bimix.Infrastructure.Sync;
|
using Bimix.Infrastructure.Sync;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -13,6 +18,43 @@ builder.Services.AddControllers();
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
// Start auth section
|
||||||
|
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
||||||
|
var secretKey = jwtSettings["SecretKey"];
|
||||||
|
var issuer = jwtSettings["Issuer"];
|
||||||
|
var audience = jwtSettings["Audience"];
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = issuer,
|
||||||
|
ValidAudience = audience,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
builder.Services.AddAuthentication();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<GoogleAuthService>();
|
||||||
|
builder.Services.AddScoped<JwtTokenService>();
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// End auth section
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@@ -22,6 +64,8 @@ if (app.Environment.IsDevelopment())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
app.UseAuthorization();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.Run();
|
app.Run();
|
||||||
72
Bimix.API/Services/GoogleAuthService.cs
Normal file
72
Bimix.API/Services/GoogleAuthService.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Bimix.Domain.Entities;
|
||||||
|
using Bimix.Infrastructure.Data;
|
||||||
|
using Google.Apis.Auth;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Bimix.API.Services;
|
||||||
|
|
||||||
|
public class GoogleAuthService(BimixDbContext context, IConfiguration configuration, ILogger<GoogleAuthService> logger)
|
||||||
|
{
|
||||||
|
private readonly BimixDbContext _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.GoogleId == payload.Subject || x.Email == payload.Email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("User not found in Bimix database: {Email}", payload.Email);
|
||||||
|
return (false, null, "User not found in Bimix database");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.IsActive)
|
||||||
|
{
|
||||||
|
_logger.LogError("User is not active: {Email}", payload.Email);
|
||||||
|
return (false, null, "User is not active");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.LastLoginAt = DateTime.UtcNow;
|
||||||
|
user.FullName = payload.Name;
|
||||||
|
|
||||||
|
if (user.GoogleId != payload.Subject)
|
||||||
|
{
|
||||||
|
user.GoogleId = payload.Subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Bimix.API/Services/JwtTokenService.cs
Normal file
87
Bimix.API/Services/JwtTokenService.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Bimix.Domain.Entities;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Bimix.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.FullName),
|
||||||
|
new Claim("google_id", user.GoogleId),
|
||||||
|
new Claim("is_active", user.IsActive.ToString()),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
22
Bimix.Application/DTOModels/AuthDto.cs
Normal file
22
Bimix.Application/DTOModels/AuthDto.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Bimix.Application.DTOModels;
|
||||||
|
|
||||||
|
public class GoogleAuthRequest
|
||||||
|
{
|
||||||
|
public string? IdToken { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GoogleAuthResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public UserDto? User { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserDto {
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
public string FullName { get; set; } = default!;
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime? LastLoginAt { get; set; }
|
||||||
|
}
|
||||||
10
Bimix.Domain/Entities/User.cs
Normal file
10
Bimix.Domain/Entities/User.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Bimix.Domain.Entities;
|
||||||
|
|
||||||
|
public class User : BaseEntity
|
||||||
|
{
|
||||||
|
public string GoogleId { get; set; } = default!;
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
public string FullName { get; set; } = default!;
|
||||||
|
public bool IsActive { get; set; } = false;
|
||||||
|
public DateTime? LastLoginAt { get; set; }
|
||||||
|
}
|
||||||
@@ -7,17 +7,84 @@ public class BimixDbContext(DbContextOptions<BimixDbContext> options) : DbContex
|
|||||||
{
|
{
|
||||||
public DbSet<Product> Products { get; set; }
|
public DbSet<Product> Products { get; set; }
|
||||||
public DbSet<SyncState> SyncStates { get; set; }
|
public DbSet<SyncState> SyncStates { get; set; }
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// Product properties
|
||||||
modelBuilder.Entity<Product>().HasKey(x => x.Id);
|
modelBuilder.Entity<Product>().HasKey(x => x.Id);
|
||||||
modelBuilder.Entity<Product>().Property(x => x.Name).IsRequired().HasMaxLength(512);
|
modelBuilder.Entity<Product>().Property(x => x.Name).IsRequired().HasMaxLength(512);
|
||||||
modelBuilder.Entity<Product>().Property(x => x.Code).IsRequired().HasMaxLength(40);
|
modelBuilder.Entity<Product>().Property(x => x.Code).IsRequired().HasMaxLength(40);
|
||||||
modelBuilder.Entity<Product>().Property(x => x.Ean).IsRequired().HasMaxLength(50);
|
modelBuilder.Entity<Product>().Property(x => x.Ean).IsRequired().HasMaxLength(50);
|
||||||
modelBuilder.Entity<Product>().Property(x => x.StockAddresses).IsRequired().HasMaxLength(512);
|
modelBuilder.Entity<Product>().Property(x => x.StockAddresses).IsRequired().HasMaxLength(512);
|
||||||
|
|
||||||
|
// SyncState properties
|
||||||
modelBuilder.Entity<SyncState>().HasKey((x => x.Entity));
|
modelBuilder.Entity<SyncState>().HasKey((x => x.Entity));
|
||||||
|
|
||||||
|
// User properties
|
||||||
|
modelBuilder.Entity<User>().HasKey(x => x.Id);
|
||||||
|
modelBuilder.Entity<User>().Property(x => x.GoogleId).IsRequired().HasMaxLength(100);
|
||||||
|
modelBuilder.Entity<User>().Property(x => x.Email).IsRequired().HasMaxLength(255);
|
||||||
|
modelBuilder.Entity<User>().Property(x => x.FullName).IsRequired().HasMaxLength(255);
|
||||||
|
modelBuilder.Entity<User>().Property(x => x.IsActive).IsRequired().HasDefaultValue(false);
|
||||||
|
modelBuilder.Entity<User>().Property(x => x.LastLoginAt).IsRequired(false);
|
||||||
|
|
||||||
|
// User indexes
|
||||||
|
modelBuilder.Entity<User>().HasIndex(x => x.GoogleId).IsUnique().HasDatabaseName("IX_Users_GoogleId");
|
||||||
|
modelBuilder.Entity<User>().HasIndex(x => x.Email).IsUnique().HasDatabaseName("IX_Users_Email");
|
||||||
|
|
||||||
|
// Configure defaults for all CreatedAt and UpdatedAt in entities
|
||||||
|
ConfigureBaseEntity(modelBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureBaseEntity(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||||
|
{
|
||||||
|
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
|
||||||
|
{
|
||||||
|
modelBuilder.Entity(entityType.ClrType)
|
||||||
|
.Property(nameof(BaseEntity.CreatedAt))
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
modelBuilder.Entity(entityType.ClrType)
|
||||||
|
.Property(nameof(BaseEntity.UpdatedAt))
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int SaveChanges()
|
||||||
|
{
|
||||||
|
UpdateTimestamps();
|
||||||
|
return base.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
UpdateTimestamps();
|
||||||
|
return base.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTimestamps()
|
||||||
|
{
|
||||||
|
var entities = ChangeTracker.Entries<BaseEntity>();
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (entity.State == EntityState.Added)
|
||||||
|
{
|
||||||
|
entity.Entity.CreatedAt = DateTime.UtcNow;
|
||||||
|
entity.Entity.UpdatedAt = DateTime.UtcNow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (entity.State == EntityState.Modified)
|
||||||
|
{
|
||||||
|
entity.Entity.UpdatedAt = DateTime.UtcNow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
136
Bimix.Infrastructure/Migrations/20250718162313_AddUsersTable.Designer.cs
generated
Normal file
136
Bimix.Infrastructure/Migrations/20250718162313_AddUsersTable.Designer.cs
generated
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Bimix.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bimix.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(BimixDbContext))]
|
||||||
|
[Migration("20250718162313_AddUsersTable")]
|
||||||
|
partial class AddUsersTable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.17")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bimix.Domain.Entities.Product", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("nvarchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Ean")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("StockAddresses")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Products");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Entity")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<long>("LastSynced")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Entity");
|
||||||
|
|
||||||
|
b.ToTable("SyncStates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bimix.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("FullName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Users_Email");
|
||||||
|
|
||||||
|
b.HasIndex("GoogleId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Users_GoogleId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bimix.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUsersTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<DateTime>(
|
||||||
|
name: "UpdatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
defaultValueSql: "GETUTCDATE()",
|
||||||
|
oldClrType: typeof(DateTime),
|
||||||
|
oldType: "datetime2");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<DateTime>(
|
||||||
|
name: "CreatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
defaultValueSql: "GETUTCDATE()",
|
||||||
|
oldClrType: typeof(DateTime),
|
||||||
|
oldType: "datetime2");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
GoogleId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Email = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||||
|
FullName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
LastLoginAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_Email",
|
||||||
|
table: "Users",
|
||||||
|
column: "Email",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_GoogleId",
|
||||||
|
table: "Users",
|
||||||
|
column: "GoogleId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<DateTime>(
|
||||||
|
name: "UpdatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(DateTime),
|
||||||
|
oldType: "datetime2",
|
||||||
|
oldDefaultValueSql: "GETUTCDATE()");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<DateTime>(
|
||||||
|
name: "CreatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(DateTime),
|
||||||
|
oldType: "datetime2",
|
||||||
|
oldDefaultValueSql: "GETUTCDATE()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ namespace Bimix.Infrastructure.Migrations
|
|||||||
.HasColumnType("nvarchar(40)");
|
.HasColumnType("nvarchar(40)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime2");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
b.Property<string>("Ean")
|
b.Property<string>("Ean")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -52,7 +54,9 @@ namespace Bimix.Infrastructure.Migrations
|
|||||||
.HasColumnType("nvarchar(512)");
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
.HasColumnType("datetime2");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
@@ -71,6 +75,58 @@ namespace Bimix.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.ToTable("SyncStates");
|
b.ToTable("SyncStates");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bimix.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("FullName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Users_Email");
|
||||||
|
|
||||||
|
b.HasIndex("GoogleId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Users_GoogleId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="Pages\ProductList.razor" />
|
<Folder Include="wwwroot\images\" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
43
Bimix.UI.Shared/Components/AuthGuard.razor
Normal file
43
Bimix.UI.Shared/Components/AuthGuard.razor
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
@using Bimix.UI.Shared.Services
|
||||||
|
@inject AuthService AuthService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-center align-center" style="height: 100vh;">
|
||||||
|
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_isAuthenticated)
|
||||||
|
{
|
||||||
|
@ChildContent
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
private bool _isLoading = true;
|
||||||
|
private bool _isAuthenticated = false;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
Console.WriteLine("AuthGuard: Checking authentication...");
|
||||||
|
|
||||||
|
// ZAWSZE sprawdź localStorage przy inicjalizacji
|
||||||
|
_isAuthenticated = await AuthService.CheckAuthenticationAsync();
|
||||||
|
_isLoading = false;
|
||||||
|
|
||||||
|
Console.WriteLine($"AuthGuard: isAuthenticated={_isAuthenticated}");
|
||||||
|
|
||||||
|
if (!_isAuthenticated)
|
||||||
|
{
|
||||||
|
Console.WriteLine("AuthGuard: Redirecting to /login");
|
||||||
|
Navigation.NavigateTo("/login", replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Bimix.UI.Shared/Components/LoginCard.razor
Normal file
121
Bimix.UI.Shared/Components/LoginCard.razor
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
@using Microsoft.Extensions.Configuration
|
||||||
|
@using Bimix.UI.Shared.Services
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject IConfiguration Configuration
|
||||||
|
@inject AuthService AuthService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
<MudCard Class="login-card" Elevation="8">
|
||||||
|
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">Witaj w Bimix</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-6 text-center">
|
||||||
|
Zaloguj się używając konta Google
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudButton
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
StartIcon="@Icons.Custom.Brands.Google"
|
||||||
|
Size="Size.Large"
|
||||||
|
OnClick="HandleGoogleSignIn"
|
||||||
|
Disabled="@_isLoading">
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Zaloguj z Google</span>
|
||||||
|
}
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Class="mt-4">
|
||||||
|
@_errorMessage
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _isLoading = false;
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
private static LoginCard? _instance;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleGoogleSignIn()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = string.Empty;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var clientId = Configuration["GoogleAuth:ClientId"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(clientId))
|
||||||
|
{
|
||||||
|
throw new Exception("Google ClientId is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("initGoogleSignIn", clientId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errorMessage = "Błąd podczas logownia. Spróbuj ponownie";
|
||||||
|
_isLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public static async Task OnGoogleSignInSuccess(string idToken)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Google ID Token: {idToken}");
|
||||||
|
|
||||||
|
if (_instance != null)
|
||||||
|
{
|
||||||
|
await _instance.AuthService.SetAuthenticationAsync(idToken);
|
||||||
|
|
||||||
|
_instance._isLoading = false;
|
||||||
|
_instance._errorMessage = string.Empty;
|
||||||
|
|
||||||
|
_instance.NavigationManager.NavigateTo("/dashboard", replace:true);
|
||||||
|
|
||||||
|
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public static async Task OnGoggleSignInError(string error)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Google SignIn Error: {error}");
|
||||||
|
|
||||||
|
if (_instance != null)
|
||||||
|
{
|
||||||
|
_instance._isLoading = false;
|
||||||
|
_instance._errorMessage = "Błąd logowanie Google. Spróbuj ponownie";
|
||||||
|
await _instance.InvokeAsync(() => _instance.StateHasChanged());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-signin-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
Bimix.UI.Shared/EmptyLayout.razor
Normal file
8
Bimix.UI.Shared/EmptyLayout.razor
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<MudThemeProvider/>
|
||||||
|
<MudDialogProvider/>
|
||||||
|
<MudSnackbarProvider/>
|
||||||
|
|
||||||
|
|
||||||
|
@Body
|
||||||
@@ -1,41 +1,43 @@
|
|||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<MudThemeProvider/>
|
<AuthGuard>
|
||||||
<MudDialogProvider/>
|
<MudThemeProvider/>
|
||||||
<MudSnackbarProvider/>
|
<MudDialogProvider/>
|
||||||
|
<MudSnackbarProvider/>
|
||||||
|
|
||||||
<MudLayout>
|
<MudLayout>
|
||||||
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
||||||
<MudAppBar Elevation="0">
|
<MudAppBar Elevation="0">
|
||||||
<MudIconButton
|
<MudIconButton
|
||||||
Icon="@Icons.Material.Filled.Menu"
|
Icon="@Icons.Material.Filled.Menu"
|
||||||
Color="Color.Inherit"
|
Color="Color.Inherit"
|
||||||
Edge="Edge.Start"
|
Edge="Edge.Start"
|
||||||
OnClick="ToggleDrawer"
|
OnClick="ToggleDrawer"
|
||||||
Class="mud-hidden-md-up"/>
|
Class="mud-hidden-md-up"/>
|
||||||
<MudSpacer/>
|
<MudSpacer/>
|
||||||
<MudText Typo="Typo.h6">Bimix</MudText>
|
<MudText Typo="Typo.h6">Bimix</MudText>
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
|
||||||
<MudDrawer @bind-Open="_drawerOpen"
|
<MudDrawer @bind-Open="_drawerOpen"
|
||||||
Anchor="Anchor.Start"
|
Anchor="Anchor.Start"
|
||||||
Variant="@_drawerVariant"
|
Variant="@_drawerVariant"
|
||||||
Elevation="1"
|
Elevation="1"
|
||||||
ClipMode="DrawerClipMode.Always"
|
ClipMode="DrawerClipMode.Always"
|
||||||
Class="mud-width-250">
|
Class="mud-width-250">
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||||
<MudNavLink Href="/products" Icon="@Icons.Material.Filled.Inventory">Products</MudNavLink>
|
<MudNavLink Href="/products" Icon="@Icons.Material.Filled.Inventory">Products</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
|
|
||||||
<MudMainContent>
|
<MudMainContent>
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
|
||||||
@Body
|
@Body
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
</MudLayout>
|
</MudLayout>
|
||||||
|
</AuthGuard>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
@@ -63,4 +65,5 @@
|
|||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
46
Bimix.UI.Shared/Pages/LoginPage.razor
Normal file
46
Bimix.UI.Shared/Pages/LoginPage.razor
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@page "/login"
|
||||||
|
@layout EmptyLayout
|
||||||
|
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-container">
|
||||||
|
<LoginCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/Bimix.UI.Shared/images/login-background.jpg') no-repeat center;
|
||||||
|
background-size: cover;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
86
Bimix.UI.Shared/Services/AuthService.cs
Normal file
86
Bimix.UI.Shared/Services/AuthService.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Bimix.UI.Shared.Services;
|
||||||
|
|
||||||
|
public class AuthService
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private bool? _isAuthenticated;
|
||||||
|
|
||||||
|
public event Action<bool>? AuthenticationStateChanged;
|
||||||
|
|
||||||
|
public AuthService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAuthenticated => _isAuthenticated ?? false;
|
||||||
|
|
||||||
|
public async Task<bool> CheckAuthenticationAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
||||||
|
_isAuthenticated = !string.IsNullOrEmpty(token);
|
||||||
|
|
||||||
|
Console.WriteLine($"AuthService.CheckAuthentication: token={(!string.IsNullOrEmpty(token) ? "EXISTS" : "NULL")}, isAuth={_isAuthenticated}");
|
||||||
|
|
||||||
|
return _isAuthenticated.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"AuthService.CheckAuthentication ERROR: {ex.Message}");
|
||||||
|
_isAuthenticated = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAuthenticationAsync(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "google_token", token);
|
||||||
|
_isAuthenticated = true;
|
||||||
|
Console.WriteLine($"AuthService.SetAuthentication: token saved, isAuth={_isAuthenticated}");
|
||||||
|
AuthenticationStateChanged?.Invoke(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"AuthService.SetAuthentication ERROR: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAuthenticationAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "google_token");
|
||||||
|
_isAuthenticated = false;
|
||||||
|
Console.WriteLine($"AuthService.ClearAuthentication: token removed");
|
||||||
|
AuthenticationStateChanged?.Invoke(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"AuthService.ClearAuthentication ERROR: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetTokenAsync()
|
||||||
|
{
|
||||||
|
if (_isAuthenticated != true)
|
||||||
|
{
|
||||||
|
await CheckAuthenticationAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isAuthenticated != true) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "google_token");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Bimix.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
7
Bimix.UI.Shared/Services/GoogleAuthConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Bimix.UI.Shared.Services;
|
||||||
|
|
||||||
|
// TODO it's a good place for this file?
|
||||||
|
public class GoogleAuthConfig
|
||||||
|
{
|
||||||
|
public string ClientId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
BIN
Bimix.UI.Shared/wwwroot/images/login-background.jpg
Normal file
BIN
Bimix.UI.Shared/wwwroot/images/login-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 965 KiB |
35
Bimix.UI.Shared/wwwroot/js/auth.js
Normal file
35
Bimix.UI.Shared/wwwroot/js/auth.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
window.initGoogleSignIn = async function(clientId) {
|
||||||
|
try {
|
||||||
|
if (!clientId) {
|
||||||
|
throw new Error('ClientId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicjalizacja Google Sign-In z dynamicznym ClientId
|
||||||
|
google.accounts.id.initialize({
|
||||||
|
client_id: clientId,
|
||||||
|
callback: handleGoogleResponse,
|
||||||
|
auto_select: false,
|
||||||
|
cancel_on_tap_outside: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wyświetl popup logowania
|
||||||
|
google.accounts.id.prompt((notification) => {
|
||||||
|
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
||||||
|
console.log('Google Sign-In popup not displayed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Google Sign-In initialization error:', error);
|
||||||
|
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInError', error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleGoogleResponse(response) {
|
||||||
|
if (response.credential) {
|
||||||
|
// Token otrzymany - wyślij do Blazor
|
||||||
|
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInSuccess', response.credential);
|
||||||
|
} else {
|
||||||
|
DotNet.invokeMethodAsync('Bimix.UI.Shared', 'OnGoogleSignInError', 'No credential received');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
|
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
|
<script src="_content/Bimix.UI.Shared/js/auth.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Bimix.UI.Shared;
|
using Bimix.UI.Shared;
|
||||||
using Bimix.UI.Shared.Extensions;
|
using Bimix.UI.Shared.Extensions;
|
||||||
using Bimix.UI.Shared.Interfaces;
|
using Bimix.UI.Shared.Interfaces;
|
||||||
|
using Bimix.UI.Shared.Services;
|
||||||
using Bimix.UI.Web.Components;
|
using Bimix.UI.Web.Components;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ builder.Services.AddMudServices();
|
|||||||
builder.Services.AddSharedServices("http://localhost:7142");
|
builder.Services.AddSharedServices("http://localhost:7142");
|
||||||
|
|
||||||
builder.Services.AddSingleton<IScannerService, NoOpScannerService>();
|
builder.Services.AddSingleton<IScannerService, NoOpScannerService>();
|
||||||
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user