This commit is contained in:
26
BimAI.API/BimAI.API.csproj
Normal file
26
BimAI.API/BimAI.API.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Apis.Auth" Version="1.70.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.17" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.17">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<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>
|
||||
<ProjectReference Include="..\BimAI.Application\BimAI.Application.csproj" />
|
||||
<ProjectReference Include="..\BimAI.Infrastructure\BimAI.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
BimAI.API/BimAI.API.http
Normal file
6
BimAI.API/BimAI.API.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@BimAI.API_HostAddress = http://localhost:5090
|
||||
|
||||
GET {{BimAI.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
96
BimAI.API/Controllers/AuthController.cs
Normal file
96
BimAI.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Security.Claims;
|
||||
using BimAI.API.Services;
|
||||
using BimAI.Application.DTOModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BimAI.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
71
BimAI.API/Controllers/ProductsController.cs
Normal file
71
BimAI.API/Controllers/ProductsController.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using BimAI.Application.DTOModels;
|
||||
using BimAI.Application.DTOModels.Common;
|
||||
using BimAI.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BimAI.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductsController(BimAIDbContext context) : ControllerBase
|
||||
{
|
||||
private readonly BimAIDbContext _context = context;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts([FromQuery] ProductFilterRequest request)
|
||||
{
|
||||
var query = _context.Products.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
var searchTerm = request.Search.ToLower();
|
||||
query = query.Where(x =>
|
||||
x.Name.ToLower().Contains(searchTerm) ||
|
||||
(x.Code != null && x.Code.ToLower().Contains(searchTerm)) ||
|
||||
(x.Ean != null && x.Ean.ToLower().Contains(searchTerm))
|
||||
);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
query = query.Where(x => x.Name.ToLower().Contains(request.Name.ToLower()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Code))
|
||||
{
|
||||
query = query.Where(x => x.Code != null && x.Code.ToLower().Contains(request.Code.ToLower()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Ean))
|
||||
{
|
||||
query = query.Where(x => x.Ean != null && x.Ean.ToLower().Contains(request.Ean.ToLower()));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderBy(x => x.Name)
|
||||
.Skip((request.Page -1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new ProductDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name,
|
||||
Code = x.Code ?? string.Empty,
|
||||
Ean = x.Ean ?? string.Empty,
|
||||
StockAddresses = x.StockAddresses ?? string.Empty,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new PagedResult<ProductDto>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = request.Page,
|
||||
PageSize = request.PageSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
16
BimAI.API/Controllers/SyncController.cs
Normal file
16
BimAI.API/Controllers/SyncController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using BimAI.Infrastructure.Sync;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BimAI.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SyncController(ProductSyncService productSyncService) : ControllerBase
|
||||
{
|
||||
[HttpPost("run-product-sync")]
|
||||
public async Task<IActionResult> RunProductSync()
|
||||
{
|
||||
await productSyncService.RunAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
71
BimAI.API/Program.cs
Normal file
71
BimAI.API/Program.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Text;
|
||||
using BimAI.API.Services;
|
||||
using BimAI.Infrastructure.Data;
|
||||
using BimAI.Infrastructure.Sync;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
builder.Services.AddDbContext<BimAIDbContext>(options => options.UseSqlServer(connectionString));
|
||||
builder.Services.AddScoped<ProductSyncService>();
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
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();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseCors("AllowAll");
|
||||
app.UseAuthorization();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
14
BimAI.API/Properties/launchSettings.json
Normal file
14
BimAI.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"dev": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:7142;http://0.0.0.0:7142",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
BimAI.API/Services/GoogleAuthService.cs
Normal file
72
BimAI.API/Services/GoogleAuthService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using BimAI.Domain.Entities;
|
||||
using BimAI.Infrastructure.Data;
|
||||
using Google.Apis.Auth;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BimAI.API.Services;
|
||||
|
||||
public class GoogleAuthService(BimAIDbContext context, IConfiguration configuration, ILogger<GoogleAuthService> logger)
|
||||
{
|
||||
private readonly BimAIDbContext _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 BimAI database: {Email}", payload.Email);
|
||||
return (false, null, "User not found in BimAI 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
86
BimAI.API/Services/JwtTokenService.cs
Normal file
86
BimAI.API/Services/JwtTokenService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using BimAI.Domain.Entities;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace BimAI.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
BimAI.API/appsettings.Development.json
Normal file
27
BimAI.API/appsettings.Development.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost,1433;Database=bimai;User Id=sa;Password=9832&^*&huihj;TrustServerCertificate=True;"
|
||||
},
|
||||
"E5_CRM": {
|
||||
"ApiKey": "7e50a8a5-f01f-4fbc-8c1b-59f3fc474bb5"
|
||||
},
|
||||
"GoogleAuth": {
|
||||
"ClientId": "1037727384847-t1l2au6du34kdckamro81guklk17cjah.apps.googleusercontent.com"
|
||||
},
|
||||
"JwtSettings": {
|
||||
"SecretKey": "BimAISuperSecretKeyThatMustBeAtLeast32CharactersLong123456789",
|
||||
"Issuer": "BimAI.API",
|
||||
"Audience": "BimAI.Clients",
|
||||
"ExpiryDays": 7
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Infrastructure": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
BimAI.API/appsettings.json
Normal file
9
BimAI.API/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user