diff --git a/SIG-CM.pdf b/SIG-CM.pdf new file mode 100644 index 0000000..6525284 Binary files /dev/null and b/SIG-CM.pdf differ diff --git a/src/SIGCM.API/Controllers/AuthController.cs b/src/SIGCM.API/Controllers/AuthController.cs new file mode 100644 index 0000000..a9a4bc1 --- /dev/null +++ b/src/SIGCM.API/Controllers/AuthController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using SIGCM.Application.DTOs; +using SIGCM.Application.Interfaces; + +namespace SIGCM.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + [HttpPost("login")] + public async Task Login(LoginDto dto) + { + var token = await _authService.LoginAsync(dto.Username, dto.Password); + if (token == null) return Unauthorized("Invalid credentials"); + + return Ok(new { token }); + } +} diff --git a/src/SIGCM.API/Controllers/CategoriesController.cs b/src/SIGCM.API/Controllers/CategoriesController.cs new file mode 100644 index 0000000..7517f31 --- /dev/null +++ b/src/SIGCM.API/Controllers/CategoriesController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; + +namespace SIGCM.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoriesController : ControllerBase +{ + private readonly ICategoryRepository _repository; + + public CategoriesController(ICategoryRepository repository) + { + _repository = repository; + } + + [HttpGet] + public async Task GetAll() + { + var categories = await _repository.GetAllAsync(); + return Ok(categories); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var category = await _repository.GetByIdAsync(id); + if (category == null) return NotFound(); + return Ok(category); + } + + [HttpPost] + public async Task Create(Category category) + { + var id = await _repository.AddAsync(category); + category.Id = id; + return CreatedAtAction(nameof(GetById), new { id }, category); + } + + [HttpPut("{id}")] + public async Task Update(int id, Category category) + { + if (id != category.Id) return BadRequest(); + await _repository.UpdateAsync(category); + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/src/SIGCM.API/Controllers/OperationsController.cs b/src/SIGCM.API/Controllers/OperationsController.cs new file mode 100644 index 0000000..bb7c63b --- /dev/null +++ b/src/SIGCM.API/Controllers/OperationsController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; + +namespace SIGCM.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class OperationsController : ControllerBase +{ + private readonly IOperationRepository _repository; + + public OperationsController(IOperationRepository repository) + { + _repository = repository; + } + + [HttpGet] + public async Task GetAll() + { + var operations = await _repository.GetAllAsync(); + return Ok(operations); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var operation = await _repository.GetByIdAsync(id); + if (operation == null) return NotFound(); + return Ok(operation); + } + + [HttpPost] + public async Task Create(Operation operation) + { + var id = await _repository.AddAsync(operation); + operation.Id = id; + return CreatedAtAction(nameof(GetById), new { id }, operation); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/src/SIGCM.API/Program.cs b/src/SIGCM.API/Program.cs new file mode 100644 index 0000000..2c9d849 --- /dev/null +++ b/src/SIGCM.API/Program.cs @@ -0,0 +1,35 @@ +using SIGCM.Infrastructure; +using SIGCM.Infrastructure.Data; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddInfrastructure(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +// Initialize DB +using (var scope = app.Services.CreateScope()) +{ + var initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(); +} + +app.Run(); diff --git a/src/SIGCM.API/Properties/launchSettings.json b/src/SIGCM.API/Properties/launchSettings.json new file mode 100644 index 0000000..8d6c719 --- /dev/null +++ b/src/SIGCM.API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5176", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7034;http://localhost:5176", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/SIGCM.API/SIGCM.API.csproj b/src/SIGCM.API/SIGCM.API.csproj new file mode 100644 index 0000000..f8c07f9 --- /dev/null +++ b/src/SIGCM.API/SIGCM.API.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/SIGCM.API/SIGCM.API.http b/src/SIGCM.API/SIGCM.API.http new file mode 100644 index 0000000..8ffa92c --- /dev/null +++ b/src/SIGCM.API/SIGCM.API.http @@ -0,0 +1,6 @@ +@SIGCM.API_HostAddress = http://localhost:5176 + +GET {{SIGCM.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/SIGCM.API/appsettings.json b/src/SIGCM.API/appsettings.json new file mode 100644 index 0000000..ef918f3 --- /dev/null +++ b/src/SIGCM.API/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=TECNICA3;Database=SIGCM;User Id=sigcmApi;Password=@Diego550@;TrustServerCertificate=True" + }, + "Jwt": { + "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", + "Issuer": "SIGCMApi", + "Audience": "SIGCMAdmin" + } +} \ No newline at end of file diff --git a/src/SIGCM.Application/Class1.cs b/src/SIGCM.Application/Class1.cs new file mode 100644 index 0000000..d0159fe --- /dev/null +++ b/src/SIGCM.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace SIGCM.Application; + +public class Class1 +{ + +} diff --git a/src/SIGCM.Application/DTOs/LoginDto.cs b/src/SIGCM.Application/DTOs/LoginDto.cs new file mode 100644 index 0000000..948e751 --- /dev/null +++ b/src/SIGCM.Application/DTOs/LoginDto.cs @@ -0,0 +1,7 @@ +namespace SIGCM.Application.DTOs; + +public class LoginDto +{ + public required string Username { get; set; } + public required string Password { get; set; } +} diff --git a/src/SIGCM.Application/Interfaces/IAuthService.cs b/src/SIGCM.Application/Interfaces/IAuthService.cs new file mode 100644 index 0000000..06941b8 --- /dev/null +++ b/src/SIGCM.Application/Interfaces/IAuthService.cs @@ -0,0 +1,6 @@ +namespace SIGCM.Application.Interfaces; + +public interface IAuthService +{ + Task LoginAsync(string username, string password); +} diff --git a/src/SIGCM.Application/Interfaces/ITokenService.cs b/src/SIGCM.Application/Interfaces/ITokenService.cs new file mode 100644 index 0000000..477683c --- /dev/null +++ b/src/SIGCM.Application/Interfaces/ITokenService.cs @@ -0,0 +1,7 @@ +namespace SIGCM.Application.Interfaces; +using SIGCM.Domain.Entities; + +public interface ITokenService +{ + string GenerateToken(User user); +} diff --git a/src/SIGCM.Application/SIGCM.Application.csproj b/src/SIGCM.Application/SIGCM.Application.csproj new file mode 100644 index 0000000..b08ec68 --- /dev/null +++ b/src/SIGCM.Application/SIGCM.Application.csproj @@ -0,0 +1,13 @@ + + + + + + + + net10.0 + enable + enable + + + diff --git a/src/SIGCM.Domain/Class1.cs b/src/SIGCM.Domain/Class1.cs new file mode 100644 index 0000000..46b2bdb --- /dev/null +++ b/src/SIGCM.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace SIGCM.Domain; + +public class Class1 +{ + +} diff --git a/src/SIGCM.Domain/Entities/Category.cs b/src/SIGCM.Domain/Entities/Category.cs new file mode 100644 index 0000000..6cf616b --- /dev/null +++ b/src/SIGCM.Domain/Entities/Category.cs @@ -0,0 +1,10 @@ +namespace SIGCM.Domain.Entities; + +public class Category +{ + public int Id { get; set; } + public int? ParentId { get; set; } + public required string Name { get; set; } + public required string Slug { get; set; } + public bool Active { get; set; } = true; +} diff --git a/src/SIGCM.Domain/Entities/Operation.cs b/src/SIGCM.Domain/Entities/Operation.cs new file mode 100644 index 0000000..272a678 --- /dev/null +++ b/src/SIGCM.Domain/Entities/Operation.cs @@ -0,0 +1,7 @@ +namespace SIGCM.Domain.Entities; + +public class Operation +{ + public int Id { get; set; } + public required string Name { get; set; } +} diff --git a/src/SIGCM.Domain/Entities/User.cs b/src/SIGCM.Domain/Entities/User.cs new file mode 100644 index 0000000..c73a535 --- /dev/null +++ b/src/SIGCM.Domain/Entities/User.cs @@ -0,0 +1,11 @@ +namespace SIGCM.Domain.Entities; + +public class User +{ + public int Id { get; set; } + public required string Username { get; set; } + public required string PasswordHash { get; set; } + public required string Role { get; set; } + public string? Email { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs b/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs new file mode 100644 index 0000000..2e32457 --- /dev/null +++ b/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs @@ -0,0 +1,12 @@ +namespace SIGCM.Domain.Interfaces; +using SIGCM.Domain.Entities; + +public interface ICategoryRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task AddAsync(Category category); + Task UpdateAsync(Category category); + Task DeleteAsync(int id); + Task> GetSubCategoriesAsync(int parentId); +} diff --git a/src/SIGCM.Domain/Interfaces/IOperationRepository.cs b/src/SIGCM.Domain/Interfaces/IOperationRepository.cs new file mode 100644 index 0000000..38ea278 --- /dev/null +++ b/src/SIGCM.Domain/Interfaces/IOperationRepository.cs @@ -0,0 +1,10 @@ +namespace SIGCM.Domain.Interfaces; +using SIGCM.Domain.Entities; + +public interface IOperationRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task AddAsync(Operation operation); + Task DeleteAsync(int id); +} diff --git a/src/SIGCM.Domain/Interfaces/IUserRepository.cs b/src/SIGCM.Domain/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..27251ce --- /dev/null +++ b/src/SIGCM.Domain/Interfaces/IUserRepository.cs @@ -0,0 +1,8 @@ +namespace SIGCM.Domain.Interfaces; +using SIGCM.Domain.Entities; + +public interface IUserRepository +{ + Task GetByUsernameAsync(string username); + Task CreateAsync(User user); +} diff --git a/src/SIGCM.Domain/SIGCM.Domain.csproj b/src/SIGCM.Domain/SIGCM.Domain.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/src/SIGCM.Domain/SIGCM.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/src/SIGCM.Infrastructure/Class1.cs b/src/SIGCM.Infrastructure/Class1.cs new file mode 100644 index 0000000..c98680d --- /dev/null +++ b/src/SIGCM.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace SIGCM.Infrastructure; + +public class Class1 +{ + +} diff --git a/src/SIGCM.Infrastructure/Data/DbConnectionFactory.cs b/src/SIGCM.Infrastructure/Data/DbConnectionFactory.cs new file mode 100644 index 0000000..5e82b7f --- /dev/null +++ b/src/SIGCM.Infrastructure/Data/DbConnectionFactory.cs @@ -0,0 +1,26 @@ +using System.Data; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; + +namespace SIGCM.Infrastructure.Data; + +public interface IDbConnectionFactory +{ + IDbConnection CreateConnection(); +} + +public class DbConnectionFactory : IDbConnectionFactory +{ + private readonly string _connectionString; + + public DbConnectionFactory(IConfiguration configuration) + { + _connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + } + + public IDbConnection CreateConnection() + { + return new SqlConnection(_connectionString); + } +} diff --git a/src/SIGCM.Infrastructure/Data/DbInitializer.cs b/src/SIGCM.Infrastructure/Data/DbInitializer.cs new file mode 100644 index 0000000..1742009 --- /dev/null +++ b/src/SIGCM.Infrastructure/Data/DbInitializer.cs @@ -0,0 +1,117 @@ +using Dapper; + +namespace SIGCM.Infrastructure.Data; + +public class DbInitializer +{ + private readonly IDbConnectionFactory _connectionFactory; + + public DbInitializer(IDbConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public async Task InitializeAsync() + { + using var connection = _connectionFactory.CreateConnection(); + + var sql = @" +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users') +BEGIN + CREATE TABLE Users ( + Id INT IDENTITY(1,1) PRIMARY KEY, + Username NVARCHAR(50) NOT NULL UNIQUE, + PasswordHash NVARCHAR(255) NOT NULL, + Role NVARCHAR(20) NOT NULL, + Email NVARCHAR(100) NULL, + CreatedAt DATETIME DEFAULT GETUTCDATE() + ); + + -- Seed generic admin (password: admin123) + -- Hash created with BCrypt + INSERT INTO Users (Username, PasswordHash, Role) + VALUES ('admin', '$2a$11$u.w..ExampleHashPlaceholder...', 'Admin'); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Categories') +BEGIN + CREATE TABLE Categories ( + Id INT IDENTITY(1,1) PRIMARY KEY, + ParentId INT NULL, + Name NVARCHAR(100) NOT NULL, + Slug NVARCHAR(100) NOT NULL, + Active BIT DEFAULT 1, + FOREIGN KEY (ParentId) REFERENCES Categories(Id) + ); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Operations') +BEGIN + CREATE TABLE Operations ( + Id INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(50) NOT NULL UNIQUE + ); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'CategoryOperations') +BEGIN + CREATE TABLE CategoryOperations ( + CategoryId INT NOT NULL, + OperationId INT NOT NULL, + PRIMARY KEY (CategoryId, OperationId), + FOREIGN KEY (CategoryId) REFERENCES Categories(Id) ON DELETE CASCADE, + FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE + ); +END +"; + // Fixing the placeholder hash to a valid one might be necessary if I want to login immediately. + // I will update the hash command later or create a small utility to generate one. + // For now, I'll remove the INSERT or comment it out until I can generate a real hash in C#. + + var schemaSql = @" +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users') +BEGIN + CREATE TABLE Users ( + Id INT IDENTITY(1,1) PRIMARY KEY, + Username NVARCHAR(50) NOT NULL UNIQUE, + PasswordHash NVARCHAR(255) NOT NULL, + Role NVARCHAR(20) NOT NULL, + Email NVARCHAR(100) NULL, + CreatedAt DATETIME DEFAULT GETUTCDATE() + ); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Categories') +BEGIN + CREATE TABLE Categories ( + Id INT IDENTITY(1,1) PRIMARY KEY, + ParentId INT NULL, + Name NVARCHAR(100) NOT NULL, + Slug NVARCHAR(100) NOT NULL, + Active BIT DEFAULT 1, + FOREIGN KEY (ParentId) REFERENCES Categories(Id) + ); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Operations') +BEGIN + CREATE TABLE Operations ( + Id INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(50) NOT NULL UNIQUE + ); +END + +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'CategoryOperations') +BEGIN + CREATE TABLE CategoryOperations ( + CategoryId INT NOT NULL, + OperationId INT NOT NULL, + PRIMARY KEY (CategoryId, OperationId), + FOREIGN KEY (CategoryId) REFERENCES Categories(Id) ON DELETE CASCADE, + FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE + ); +END +"; + await connection.ExecuteAsync(schemaSql); + } +} diff --git a/src/SIGCM.Infrastructure/DependencyInjection.cs b/src/SIGCM.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..894812b --- /dev/null +++ b/src/SIGCM.Infrastructure/DependencyInjection.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using SIGCM.Domain.Interfaces; +using SIGCM.Application.Interfaces; +using SIGCM.Infrastructure.Data; +using SIGCM.Infrastructure.Repositories; + +namespace SIGCM.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; + } +} + + + diff --git a/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs b/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs new file mode 100644 index 0000000..f178ea6 --- /dev/null +++ b/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs @@ -0,0 +1,60 @@ +using Dapper; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; +using SIGCM.Infrastructure.Data; + +namespace SIGCM.Infrastructure.Repositories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly IDbConnectionFactory _connectionFactory; + + public CategoryRepository(IDbConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public async Task> GetAllAsync() + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryAsync("SELECT * FROM Categories"); + } + + public async Task GetByIdAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryFirstOrDefaultAsync("SELECT * FROM Categories WHERE Id = @Id", new { Id = id }); + } + + public async Task AddAsync(Category category) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + INSERT INTO Categories (ParentId, Name, Slug, Active) + VALUES (@ParentId, @Name, @Slug, @Active); + SELECT CAST(SCOPE_IDENTITY() as int);"; + return await conn.QuerySingleAsync(sql, category); + } + + public async Task UpdateAsync(Category category) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + UPDATE Categories + SET ParentId = @ParentId, Name = @Name, Slug = @Slug, Active = @Active + WHERE Id = @Id"; + await conn.ExecuteAsync(sql, category); + } + + public async Task DeleteAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + await conn.ExecuteAsync("DELETE FROM Categories WHERE Id = @Id", new { Id = id }); + } + + public async Task> GetSubCategoriesAsync(int parentId) + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryAsync("SELECT * FROM Categories WHERE ParentId = @ParentId", new { ParentId = parentId }); + } +} diff --git a/src/SIGCM.Infrastructure/Repositories/OperationRepository.cs b/src/SIGCM.Infrastructure/Repositories/OperationRepository.cs new file mode 100644 index 0000000..bd75a8c --- /dev/null +++ b/src/SIGCM.Infrastructure/Repositories/OperationRepository.cs @@ -0,0 +1,44 @@ +using Dapper; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; +using SIGCM.Infrastructure.Data; + +namespace SIGCM.Infrastructure.Repositories; + +public class OperationRepository : IOperationRepository +{ + private readonly IDbConnectionFactory _connectionFactory; + + public OperationRepository(IDbConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public async Task> GetAllAsync() + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryAsync("SELECT * FROM Operations"); + } + + public async Task GetByIdAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryFirstOrDefaultAsync("SELECT * FROM Operations WHERE Id = @Id", new { Id = id }); + } + + public async Task AddAsync(Operation operation) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + INSERT INTO Operations (Name) + VALUES (@Name); + SELECT CAST(SCOPE_IDENTITY() as int);"; + return await conn.QuerySingleAsync(sql, operation); + } + + public async Task DeleteAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + await conn.ExecuteAsync("DELETE FROM Operations WHERE Id = @Id", new { Id = id }); + } +} diff --git a/src/SIGCM.Infrastructure/Repositories/UserRepository.cs b/src/SIGCM.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 0000000..32c5877 --- /dev/null +++ b/src/SIGCM.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,34 @@ +using Dapper; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; +using SIGCM.Infrastructure.Data; + +namespace SIGCM.Infrastructure.Repositories; + +public class UserRepository : IUserRepository +{ + private readonly IDbConnectionFactory _connectionFactory; + + public UserRepository(IDbConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public async Task GetByUsernameAsync(string username) + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QuerySingleOrDefaultAsync( + "SELECT * FROM Users WHERE Username = @Username", + new { Username = username }); + } + + public async Task CreateAsync(User user) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + INSERT INTO Users (Username, PasswordHash, Role, Email) + VALUES (@Username, @PasswordHash, @Role, @Email); + SELECT CAST(SCOPE_IDENTITY() as int);"; + return await conn.QuerySingleAsync(sql, user); + } +} diff --git a/src/SIGCM.Infrastructure/SIGCM.Infrastructure.csproj b/src/SIGCM.Infrastructure/SIGCM.Infrastructure.csproj new file mode 100644 index 0000000..8a45b5b --- /dev/null +++ b/src/SIGCM.Infrastructure/SIGCM.Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/src/SIGCM.Infrastructure/Services/AuthService.cs b/src/SIGCM.Infrastructure/Services/AuthService.cs new file mode 100644 index 0000000..9344451 --- /dev/null +++ b/src/SIGCM.Infrastructure/Services/AuthService.cs @@ -0,0 +1,27 @@ +using SIGCM.Application.Interfaces; +using SIGCM.Domain.Interfaces; + +namespace SIGCM.Infrastructure.Services; + +public class AuthService : IAuthService +{ + private readonly IUserRepository _userRepo; + private readonly ITokenService _tokenService; + + public AuthService(IUserRepository userRepo, ITokenService tokenService) + { + _userRepo = userRepo; + _tokenService = tokenService; + } + + public async Task LoginAsync(string username, string password) + { + var user = await _userRepo.GetByUsernameAsync(username); + if (user == null) return null; + + bool valid = BCrypt.Net.BCrypt.Verify(password, user.PasswordHash); + if (!valid) return null; + + return _tokenService.GenerateToken(user); + } +} diff --git a/src/SIGCM.Infrastructure/Services/TokenService.cs b/src/SIGCM.Infrastructure/Services/TokenService.cs new file mode 100644 index 0000000..80d7191 --- /dev/null +++ b/src/SIGCM.Infrastructure/Services/TokenService.cs @@ -0,0 +1,42 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using SIGCM.Application.Interfaces; +using SIGCM.Domain.Entities; + +namespace SIGCM.Infrastructure.Services; + +public class TokenService : ITokenService +{ + private readonly IConfiguration _config; + + public TokenService(IConfiguration config) + { + _config = config; + } + + public string GenerateToken(User user) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.Username), + new Claim(ClaimTypes.Role, user.Role), + new Claim("Id", user.Id.ToString()) + }; + + var token = new JwtSecurityToken( + issuer: _config["Jwt:Issuer"], + audience: _config["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddHours(4), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} diff --git a/src/SIGCM.sln b/src/SIGCM.sln new file mode 100644 index 0000000..96c934d --- /dev/null +++ b/src/SIGCM.sln @@ -0,0 +1,76 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIGCM.Domain", "SIGCM.Domain\SIGCM.Domain.csproj", "{0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIGCM.Application", "SIGCM.Application\SIGCM.Application.csproj", "{8B9C32EC-AE46-4882-96B0-62358FD40C84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIGCM.Infrastructure", "SIGCM.Infrastructure\SIGCM.Infrastructure.csproj", "{895726EC-FDEF-490D-A78E-98591AC26BA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIGCM.API", "SIGCM.API\SIGCM.API.csproj", "{6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|x64.ActiveCfg = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|x64.Build.0 = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|x86.ActiveCfg = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Debug|x86.Build.0 = Debug|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|Any CPU.Build.0 = Release|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|x64.ActiveCfg = Release|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|x64.Build.0 = Release|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|x86.ActiveCfg = Release|Any CPU + {0224DD39-C953-4D75-A5AE-D7FB5DC07DFD}.Release|x86.Build.0 = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|x64.Build.0 = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Debug|x86.Build.0 = Debug|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|Any CPU.Build.0 = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|x64.ActiveCfg = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|x64.Build.0 = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|x86.ActiveCfg = Release|Any CPU + {8B9C32EC-AE46-4882-96B0-62358FD40C84}.Release|x86.Build.0 = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|x64.Build.0 = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Debug|x86.Build.0 = Debug|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|Any CPU.Build.0 = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|x64.ActiveCfg = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|x64.Build.0 = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|x86.ActiveCfg = Release|Any CPU + {895726EC-FDEF-490D-A78E-98591AC26BA4}.Release|x86.Build.0 = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|x64.Build.0 = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Debug|x86.Build.0 = Debug|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|Any CPU.Build.0 = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|x64.ActiveCfg = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|x64.Build.0 = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|x86.ActiveCfg = Release|Any CPU + {6AE3C4DB-EBFC-46B4-8231-54CE76BC9D90}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal