diff --git a/.idea/.idea.ldap-cesi/.idea/.gitignore b/.idea/.idea.ldap-cesi/.idea/.gitignore new file mode 100644 index 0000000..8700c2a --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.ldap-cesi.iml +/projectSettingsUpdater.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ldap-cesi/.idea/dataSources.xml b/.idea/.idea.ldap-cesi/.idea/dataSources.xml new file mode 100644 index 0000000..62b6f3e --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/dataSources.xml @@ -0,0 +1,18 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://192.168.1.196:5432/ldap + + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/.idea.ldap-cesi/.idea/encodings.xml b/.idea/.idea.ldap-cesi/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ldap-cesi/.idea/indexLayout.xml b/.idea/.idea.ldap-cesi/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ldap-cesi/.idea/misc.xml b/.idea/.idea.ldap-cesi/.idea/misc.xml new file mode 100644 index 0000000..0cfc445 --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/misc.xml @@ -0,0 +1,26 @@ + + + {} + + + + + + Android Lint: Security + + + Code Coverage + + + HTML + + + + + AndroidLintBadHostnameVerifier + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ldap-cesi/.idea/vcs.xml b/.idea/.idea.ldap-cesi/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.ldap-cesi/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ldap-cesi.sln b/ldap-cesi.sln new file mode 100644 index 0000000..4509d4e --- /dev/null +++ b/ldap-cesi.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ldap-cesi", "ldap-cesi\ldap-cesi.csproj", "{0AE1CE6A-DF41-4377-92A9-80E130D27F8B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0AE1CE6A-DF41-4377-92A9-80E130D27F8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AE1CE6A-DF41-4377-92A9-80E130D27F8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AE1CE6A-DF41-4377-92A9-80E130D27F8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AE1CE6A-DF41-4377-92A9-80E130D27F8B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ldap-cesi/.gitignore b/ldap-cesi/.gitignore new file mode 100644 index 0000000..3775ac8 --- /dev/null +++ b/ldap-cesi/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +appsettings.json +appsettings.Development.json +/.env +/logs \ No newline at end of file diff --git a/ldap-cesi/Configurations/Conf.cs b/ldap-cesi/Configurations/Conf.cs new file mode 100644 index 0000000..b7bd501 --- /dev/null +++ b/ldap-cesi/Configurations/Conf.cs @@ -0,0 +1,148 @@ +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using FluentValidation; +using FluentValidation.AspNetCore; +using ldap_cesi.Context; +using ldap_cesi.Repository; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Serilog; + +namespace ldap_cesi.Configurations; + +public static class Conf +{ + public static void BuildConf(this WebApplicationBuilder builder) + { + builder.AddRepositories(); + builder.AddServices(); + builder.Services.AddControllers(); + builder.AddEFCoreConfiguration(); + builder.CorseConfiguration(); + builder.AddSwagger(); + builder.AddSerilog(); + builder.AddJwt(); + builder.Services.AddAutoMapper(typeof(Program)); + + } + + public static void AddRepositories(this WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + } + + public static void AddServices(this WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddValidatorsFromAssemblyContaining(); + builder.Services.AddFluentValidationAutoValidation(); + } + public static void AddEFCoreConfiguration(this WebApplicationBuilder builder) + { + string connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); + } + public static void CorseConfiguration(this WebApplicationBuilder builder) + { + builder.Services.AddCors(options => + { + options.AddPolicy("AllowSpecific", + builder => builder + .WithOrigins("http://localhost:3000") + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + + public static void AddSerilog(this WebApplicationBuilder builder) + { + var loggerConfiguration = new LoggerConfiguration() + .WriteTo.Console() + .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Hour); + var logger = loggerConfiguration.CreateLogger(); + builder.Logging.AddSerilog(logger); + builder.Services.AddLogging(); + } + + public static void AddJwt(this WebApplicationBuilder builder) + { + var rsaKeyService = builder.Services.BuildServiceProvider().GetRequiredService(); + var rsaKey = rsaKeyService.GetRsaKey(); + + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new RsaSecurityKey(rsaKey) + }; + }); + } + + public static void AddSwagger(this WebApplicationBuilder builder) + { + // Add services to the container. + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddAuthorization(); + builder.Configuration.AddEnvironmentVariables(); + builder.Services.AddSwaggerGen(c => + { + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + Description = "JWT Token. Use \"Bearer {token}\"" + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }, + new List() + } + }); + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "LDAP - Backend", + Version = "v1" + }); + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); + } +} \ No newline at end of file diff --git a/ldap-cesi/Context/PgContext.cs b/ldap-cesi/Context/PgContext.cs new file mode 100644 index 0000000..fc8c4c3 --- /dev/null +++ b/ldap-cesi/Context/PgContext.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using ldap_cesi.Entities; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Context; + +public partial class PgContext : DbContext +{ + public PgContext() + { + } + + public PgContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Roles { get; set; } + + public virtual DbSet Salaries { get; set; } + + public virtual DbSet Services { get; set; } + + public virtual DbSet Sites { get; set; } + + public virtual DbSet Utilisateurs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("role_pk"); + + entity.ToTable("role"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Nom) + .HasMaxLength(50) + .HasColumnName("nom"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("salarie_pk"); + + entity.ToTable("salarie"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Email) + .HasMaxLength(50) + .HasColumnName("email"); + entity.Property(e => e.IdService).HasColumnName("id_service"); + entity.Property(e => e.IdSite).HasColumnName("id_site"); + entity.Property(e => e.Nom) + .HasMaxLength(50) + .HasColumnName("nom"); + entity.Property(e => e.Prenom) + .HasMaxLength(50) + .HasColumnName("prenom"); + entity.Property(e => e.TelephoneFixe) + .HasMaxLength(50) + .HasColumnName("telephone_fixe"); + entity.Property(e => e.TelephonePortable) + .HasMaxLength(50) + .HasColumnName("telephone_portable"); + + entity.HasOne(d => d.IdServiceNavigation).WithMany(p => p.Salaries) + .HasForeignKey(d => d.IdService) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("salarie_service0_fk"); + + entity.HasOne(d => d.IdSiteNavigation).WithMany(p => p.Salaries) + .HasForeignKey(d => d.IdSite) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("salarie_site_fk"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("service_pk"); + + entity.ToTable("service"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Nom) + .HasMaxLength(50) + .HasColumnName("nom"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("site_pk"); + + entity.ToTable("site"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Ville) + .HasMaxLength(150) + .HasColumnName("ville"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("utilisateur_pk"); + + entity.ToTable("utilisateur"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.IdRole).HasColumnName("id_role"); + entity.Property(e => e.MotDePasse) + .HasMaxLength(50) + .HasColumnName("mot_de_passe"); + entity.Property(e => e.Nom) + .HasMaxLength(50) + .HasColumnName("nom"); + entity.Property(e => e.Email) + .HasMaxLength(50) + .HasColumnName("email"); + entity.Property(e => e.Prenom) + .HasMaxLength(50) + .HasColumnName("prenom"); + entity.Ignore(e => e.AccessToken); + + entity.HasOne(d => d.IdRoleNavigation).WithMany(p => p.Utilisateurs) + .HasForeignKey(d => d.IdRole) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("utilisateur_role_fk"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ldap-cesi/Controllers/HashController.cs b/ldap-cesi/Controllers/HashController.cs new file mode 100644 index 0000000..35c7594 --- /dev/null +++ b/ldap-cesi/Controllers/HashController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; + + +[ApiController] +[Route("api/[controller]")] +public class HashController : ControllerBase +{ + [HttpPost("hash")] + public IActionResult HashString([FromBody] string StringToHash) + { + if (string.IsNullOrEmpty(StringToHash)) + { + return BadRequest("Vous devez fournir une chaine de caractere pour la hasher."); + } + + string hashedString = BCrypt.Net.BCrypt.HashPassword(StringToHash); + + return Ok(new { HashedString = hashedString }); + } +} \ No newline at end of file diff --git a/ldap-cesi/Controllers/JwtController.cs b/ldap-cesi/Controllers/JwtController.cs new file mode 100644 index 0000000..b163375 --- /dev/null +++ b/ldap-cesi/Controllers/JwtController.cs @@ -0,0 +1,29 @@ +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; + +[ApiController] +[Route("/api/jwt")] +public class JwtController : ControllerBase +{ + private readonly IJwtService _jwtService; + + public JwtController(IJwtService jwtService) + { + _jwtService = jwtService; + } + + [HttpGet("public-key")] + public IActionResult GetPublicKey() + { + var publicKey = _jwtService.GetPublicKey(); + if (string.IsNullOrEmpty(publicKey)) + { + return BadRequest("Impossible de récupérer la clé publique"); + } + + return Ok(publicKey); + } +} \ No newline at end of file diff --git a/ldap-cesi/Controllers/RoleController.cs b/ldap-cesi/Controllers/RoleController.cs new file mode 100644 index 0000000..213995f --- /dev/null +++ b/ldap-cesi/Controllers/RoleController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc; +using ldap_cesi.DTOs.Inputs.Role; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; + +namespace ldap_cesi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class RoleController : ControllerBase + { + private readonly IRoleService _roleService; + + public RoleController(IRoleService roleService) + { + _roleService = roleService; + } + + // GET: api/Role + /// + /// Endpoint qui retourne tous les rôles + /// + /// Un tableau de rôle + [HttpGet] + [Authorize(Roles = "admin")] + public async Task GetAllRoles([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + var result = await _roleService.GetAll(pageNumber, pageSize); + return result.Success ? Ok(result) : BadRequest(result); + } + + // GET: api/Role/{id} + /// + /// Endpoint qui retourne le role + /// + /// L'id du rôle. + /// Le role update + [HttpGet("{id}")] + [Authorize(Roles = "admin")] + public async Task GetRoleById(int id) + { + var result = await _roleService.GetById(id); + return result.Success ? Ok(result) : BadRequest(result); + } + + // POST: api/Role + /// + /// Endpoint créer le Role + /// + /// Le nom du rôle. + /// Response. + [HttpPost] + [Authorize(Roles = "admin")] + public async Task CreateRole([FromBody] RoleCreateDto roleDto) + { + var result = await _roleService.Create(roleDto); + return result.Success ? Ok(result) : BadRequest(result); + } + + // PUT: api/Role + /// + /// Endpoint qui met à jour un role. + /// + /// Les informations du role à mettre à jour. Id, nom + /// Le role mis à jour. + [HttpPut] + [Authorize(Roles = "admin")] + public async Task UpdateRole([FromBody] RoleUpdateDto roleDto) + { + var result = await _roleService.Update(roleDto); + return result.Success ? Ok(result) : BadRequest(result); + } + + // DELETE: api/Role/{id} + /// + /// Endpoint qui supprime un rôle. + /// + /// L'ID du rôle à supprimer. + /// Un message de confirmation de suppression. + [HttpDelete("{id}")] + [Authorize(Roles = "admin")] + public async Task DeleteRole(int id) + { + var result = await _roleService.Delete(id); + return result.Success ? Ok(result) : BadRequest(result); + } + } +} diff --git a/ldap-cesi/Controllers/SalarieController.cs b/ldap-cesi/Controllers/SalarieController.cs new file mode 100644 index 0000000..1f831a6 --- /dev/null +++ b/ldap-cesi/Controllers/SalarieController.cs @@ -0,0 +1,146 @@ +using ldap_cesi.DTOs.Inputs.Salarie; +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; +[ApiController] +[Route("api/salaries")] +public class SalarieController : ControllerBase +{ + private ISalarieService _salarieService; + + public SalarieController(ISalarieService salarieService) + { + _salarieService = salarieService; + } + + /// + /// Endpoint qui retourne tous les salariés. + /// + /// Une liste de salariés. + [HttpGet] + public async Task GetAllSalaries([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 25) + { + var result = await _salarieService.GetAll(pageNumber, pageSize); + return result.Success ? Ok(result) : BadRequest(result); + } + + + [HttpGet("search")] + public async Task SearchSalaries( + [FromQuery] string searchTerm, + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10) + { + if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length < 2) + { + return BadRequest("Votre recherche doit contenir au moins deux caractères."); + } + + var result = await _salarieService.SearchWithRelations( + searchTerm, pageNumber, pageSize, s => s.IdServiceNavigation, s => s.IdSiteNavigation); + + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui retourne le salarié correspondant à l'ID en paramètre. + /// + /// L'ID du salarié. + /// Le salarié correspondant à l'ID. + [HttpGet("{id}")] + public async Task GetById(int id) + { + var result = await _salarieService.GetById(id); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui retourne le salarié correspondant à l'ID en paramètre. Avec le nom de service et de site auxquels il apaprtient. + /// + /// L'ID du salarié. + /// Le salarié correspondant à l'ID. Ainsi que son service et son site + [HttpGet("/complet/{id}")] + public async Task GetSalarieCompletById(int id) + { + var result = await _salarieService.GetByIdWithRelations(id,s => s.IdServiceNavigation, s => s.IdSiteNavigation); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui retourne tout les salariés et les relations qu'il détient + /// + /// Tous les salariés avec leurs relations + [HttpGet("all")] + public async Task GetAllSariesWithRelations([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 25) + { + var result = await _salarieService.GetAllWithRelationsAsync(pageNumber, pageSize,s => s.IdServiceNavigation, s => s.IdSiteNavigation); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui crée un salarié. + /// + /// Les informations du salarié à créer. + /// Le salarié créé. + [HttpPost] + [Authorize(Roles = "admin")] + public async Task CreateSalarie([FromBody] SalarieCreateDto salarieInput) + { + var result = await _salarieService.Create(salarieInput); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui met à jour un salarié. + /// + /// Les informations du salarié à mettre à jour. + /// Le salarié mis à jour. + [HttpPut] + [Authorize(Roles = "admin")] + public async Task UpdateSalarie([FromBody] SalarieUpdateDto salarieInput) + { + var result = await _salarieService.Update(salarieInput); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui supprime un salarié. + /// + /// L'ID du salarié à supprimer. + /// Un message de confirmation de suppression. + [HttpDelete("{id}")] + [Authorize(Roles = "admin")] + public async Task DeleteSalarie(int id) + { + var result = await _salarieService.Delete(id); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Récupère les salariés appartenant à un site spécifique + /// + /// Identifiant du site + /// Liste des salariés du site + [HttpGet("site/{siteId}")] + public async Task GetSalariesBySite(int siteId) + { + var result = await _salarieService.GetSalariesBySite(siteId); + return result.Success ? Ok(result) : NotFound(result); + } + + /// + /// Récupère les salariés appartenant à un service spécifique + /// + /// Identifiant du service + /// Liste des salariés du service + [HttpGet("service/{serviceId}")] + public async Task GetSalariesByService(int serviceId) + { + var result = await _salarieService.GetSalariesByService(serviceId); + return result.Success ? Ok(result) : NotFound(result); + } + +} diff --git a/ldap-cesi/Controllers/ServicesController.cs b/ldap-cesi/Controllers/ServicesController.cs new file mode 100644 index 0000000..2081b99 --- /dev/null +++ b/ldap-cesi/Controllers/ServicesController.cs @@ -0,0 +1,91 @@ +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; + +[ApiController] +[Route("api/services")] +public class ServicesController : ControllerBase +{ + private IServiceService _serviceService; + + public ServicesController(IServiceService serviceService) + { + _serviceService = serviceService; + } + + /// + /// Endpoint qui retourne tous les services. + /// + /// Retourne tous les services. + [HttpGet] + public async Task GetServices([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { + var result = await _serviceService.GetAll(pageNumber, pageSize); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui retourne le service correspondant à l'id en paramètre. + /// + /// L'ID du service. + /// Le service correspondant à l'ID. + [HttpGet("{id}")] + public async Task GetServiceById(int id) + { + var result = await _serviceService.GetById(id); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui retourne le service et ses salariés correspondant à l'id en paramètre. + /// + /// L'ID du service. + /// Le service correspondant à l'ID, avec ses salariés + [HttpGet("complete/{id}")] + public async Task GetServiceByIdWithSalaries(int id) + { + var result = await _serviceService.GetByIdWithRelations(id, s=>s.Salaries); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui crée un service. + /// + /// Les informations du service à créer. + /// Le service créé. + [HttpPost] + [Authorize(Roles = "admin")] + public async Task CreateService([FromBody] ServiceCreateDto serviceInputDto) + { + var result = await _serviceService.Create(serviceInputDto); + return result.Success ? Ok(result.Data) : BadRequest(result); + } + + /// + /// Endpoint qui met à jour un service. + /// + /// Les informations du service à mettre à jour. + /// Le service mis à jour. + [HttpPut] + [Authorize(Roles = "admin")] + public async Task UpdateService([FromBody] ServiceUpdateDto serviceUpdateDto) + { + var result = await _serviceService.Update(serviceUpdateDto); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint qui supprime un service. + /// + /// L'ID du service à supprimer. + /// Un message de confirmation de suppression. + [HttpDelete("{id}")] + [Authorize(Roles = "admin")] + public async Task DeleteService(int id) + { + var result = await _serviceService.DeleteWithEntiteCheck(id); + return result.Success ? Ok(result) : BadRequest(result); + } +} \ No newline at end of file diff --git a/ldap-cesi/Controllers/SiteController.cs b/ldap-cesi/Controllers/SiteController.cs new file mode 100644 index 0000000..bcb29c8 --- /dev/null +++ b/ldap-cesi/Controllers/SiteController.cs @@ -0,0 +1,101 @@ +using ldap_cesi.DTOs.Inputs.Site; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; + +[ApiController] +[Route("/api/sites")] +public class SiteController : ControllerBase +{ + private readonly ISiteService _siteService; + + public SiteController(ISiteService siteService) + { + _siteService = siteService; + } + + // GET: api/site + /// + /// Récupère la liste de tous les sites. + /// + /// Retourne une liste de tous les sites. + [HttpGet] + public async Task GetSites([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + var result = await _siteService.GetAll(pageNumber, pageSize); + return result.Success ? Ok(result) : BadRequest(result); + } + + // GET: api/site/{id} + /// + /// Récupère un site spécifique par son identifiant. + /// + /// L'identifiant du site à récupérer. + /// Retourne le site correspondant à l'identifiant. + /// Le site a été récupéré avec succès. + /// Une erreur s'est produite lors de la récupération du site. + /// Le site n'a pas été trouvé. + [HttpGet("{id}")] + public async Task GetSite(int id) + { + var result = await _siteService.GetById(id); + return result.Success ? Ok(result) : BadRequest(result); + } + + // GET: api/site/{id} + /// + /// Récupère un site et ses salariés spécifique par son identifiant. + /// + /// L'identifiant du site à récupérer. + /// Retourne le site et ses salariés + [HttpGet("/complete/{id}")] + public async Task GetSiteWithClient(int id) + { + var result = await _siteService.GetByIdWithRelations(id, s => s.Salaries); + return result.Success ? Ok(result) : BadRequest(result); + } + + // POST: api/site + /// + /// Crée un nouveau site. + /// + /// Les données du site à créer. + /// Retourne l'identifiant du site créé. + [HttpPost] + [Authorize(Roles = "admin")] + public async Task CreateSite([FromBody] SiteCreateDto siteCreateDto) + { + var result = await _siteService.Create(siteCreateDto); + return result.Success ? Ok(result) : BadRequest(result); + } + + // PUT: api/site + /// + /// Met à jour un site existant. + /// + /// Les données du site à mettre à jour. + /// Retourne l'objet ou une erreur. + [HttpPut] + [Authorize(Roles = "admin")] + public async Task UpdateSite([FromBody] SiteUpdateDto siteUpdateDto) + { + var result = await _siteService.Update(siteUpdateDto); + return result.Success ? Ok(result) : BadRequest(result); + } + + // DELETE: api/site/{id} + /// + /// Supprime un site par son identifiant. + /// + /// L'identifiant du site à supprimer. + /// Retourne l'id, ou l'erreur + [HttpDelete("{id}")] + [Authorize(Roles = "admin")] + public async Task DeleteSite(int id) + { + var result = await _siteService.DeleteWithEntiteCheck(id); + return result.Success ? Ok(result) : BadRequest(result); + } +} \ No newline at end of file diff --git a/ldap-cesi/Controllers/UtilisateurController.cs b/ldap-cesi/Controllers/UtilisateurController.cs new file mode 100644 index 0000000..0f747bd --- /dev/null +++ b/ldap-cesi/Controllers/UtilisateurController.cs @@ -0,0 +1,131 @@ +using System.Security.Claims; +using ldap_cesi.DTOs.Inputs; +using ldap_cesi.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ldap_cesi.Controllers; +[ApiController] +[Route("api/utilisateurs")] +public class UtilisateurController : ControllerBase +{ + private readonly IUtilisateurService _utilisateurService; + private readonly IJwtService _jwtService; + public UtilisateurController(IUtilisateurService utilisateurService, IJwtService jwtService) + { + _utilisateurService = utilisateurService; + _jwtService = jwtService; + } + + + /// + /// Endpoint pour la connexion des utilisateurs. + /// + /// Les informations de connexion de l'utilisateur. + /// Un token JWT si la connexion est réussie. + [HttpPost("login")] + public async Task Login([FromBody] UtilisateurLoginDto utilisateurInput) + { + var result = await _utilisateurService.Login(utilisateurInput); + return result.Success ? Ok(result) : BadRequest(result); + } + + [HttpGet("me")] + [Authorize] + public async Task GetCurrentUser() + { + try { + // rçupère tous les claims de type NameIdentifier + var nameIdClaims = User.FindAll(ClaimTypes.NameIdentifier).ToList(); + + // cherche le claim qui contient un nombre entier + int userId = 0; + bool foundValidId = false; + + foreach (var claim in nameIdClaims) + { + if (int.TryParse(claim.Value, out userId)) + { + foundValidId = true; + break; + } + } + + if (!foundValidId) + { + return BadRequest(new { + Success = false, + Message = "Utilisateur non identifié. Aucun ID numérique trouvé." + }); + } + + // rçupère les informations utilisateur + var result = await _utilisateurService.GetById(userId); + return result.Success ? Ok(result) : BadRequest(result); + } + catch (Exception ex) { + return BadRequest(new { Success = false, Message = $"Erreur: {ex.Message}" }); + } + } + + /// + /// Endpoint pour déconnecter un utilisateur (invalider son token). + /// + /// Un statut indiquant que la déconnexion a réussi. + [HttpPost("logout")] + [Authorize] + public async Task Logout() + { + try + { + var authHeader = Request.Headers["Authorization"].ToString(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) + { + return BadRequest(new { Success = false, Message = "Token non fourni" }); + } + + var token = authHeader.Substring("Bearer ".Length).Trim(); + + // rendre le tokenm invalide + var result = await _jwtService.InvalidateToken(token); + + if (result) + { + return Ok(new { Success = true, Message = "Déconnexion réussie" }); + } + else + { + return BadRequest(new { Success = false, Message = "Échec de la déconnexion" }); + } + } + catch (Exception ex) + { + return StatusCode(500, new { Success = false, Message = "Erreur interne du serveur" }); + } + } + + /// + /// Endpoint pour récupérer tous les utilisateurs. + /// + /// Une liste d'utilisateurs. + [HttpGet] + [Authorize(Roles = "admin")] + public async Task GetUtilisateurs() + { + var result = await _utilisateurService.GetAll(); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Endpoint pour récupérer un utilisateur par son ID. + /// + /// L'ID de l'utilisateur. + /// L'utilisateur correspondant à l'ID. + [HttpGet("{id}")] + [Authorize(Roles = "admin")] + public async Task GetUtilisateurById(int id) + { + var result = await _utilisateurService.GetById(id); + return result.Success ? Ok(result) : BadRequest(result); + } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Generic/RoleDto.cs b/ldap-cesi/DTOs/Generic/RoleDto.cs new file mode 100644 index 0000000..3465bd6 --- /dev/null +++ b/ldap-cesi/DTOs/Generic/RoleDto.cs @@ -0,0 +1,8 @@ +namespace ldap_cesi.DTOs; + +public class RoleDto +{ + public int Id { get; set; } + + public string Nom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Generic/SalarieDto.cs b/ldap-cesi/DTOs/Generic/SalarieDto.cs new file mode 100644 index 0000000..542bd7c --- /dev/null +++ b/ldap-cesi/DTOs/Generic/SalarieDto.cs @@ -0,0 +1,19 @@ +namespace ldap_cesi.DTOs; + +public class SalarieDto +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public string Prenom { get; set; } = null!; + + public string TelephoneFixe { get; set; } = null!; + + public string TelephonePortable { get; set; } = null!; + + public string Email { get; set; } = null!; + + public ServiceDto Service { get; set; } + public SiteDto Site { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Generic/ServiceDto.cs b/ldap-cesi/DTOs/Generic/ServiceDto.cs new file mode 100644 index 0000000..61f567b --- /dev/null +++ b/ldap-cesi/DTOs/Generic/ServiceDto.cs @@ -0,0 +1,11 @@ +using ldap_cesi.DTOs.Outputs.Salarie; + +namespace ldap_cesi.DTOs; + +public class ServiceDto +{ + public int Id { get; set; } + + public string Nom { get; set; } + public List Salaries { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Generic/SiteDto.cs b/ldap-cesi/DTOs/Generic/SiteDto.cs new file mode 100644 index 0000000..535f886 --- /dev/null +++ b/ldap-cesi/DTOs/Generic/SiteDto.cs @@ -0,0 +1,11 @@ +using ldap_cesi.DTOs.Outputs.Salarie; + +namespace ldap_cesi.DTOs; + +public class SiteDto +{ + public int Id { get; set; } + + public string Ville { get; set; } + public ICollection Salaries { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Role/RoleCreateDto.cs b/ldap-cesi/DTOs/Inputs/Role/RoleCreateDto.cs new file mode 100644 index 0000000..e11af07 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Role/RoleCreateDto.cs @@ -0,0 +1,7 @@ + +namespace ldap_cesi.DTOs.Inputs.Role; + +public class RoleCreateDto +{ + public string Nom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Role/RoleUpdateDto.cs b/ldap-cesi/DTOs/Inputs/Role/RoleUpdateDto.cs new file mode 100644 index 0000000..c7b5b00 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Role/RoleUpdateDto.cs @@ -0,0 +1,7 @@ +namespace ldap_cesi.DTOs.Inputs.Role; + +public class RoleUpdateDto +{ + public int Id { get; set; } + public string Nom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Salarie/SalarieCreateDto.cs b/ldap-cesi/DTOs/Inputs/Salarie/SalarieCreateDto.cs new file mode 100644 index 0000000..a8ca3e7 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Salarie/SalarieCreateDto.cs @@ -0,0 +1,12 @@ +namespace ldap_cesi.DTOs.Inputs.Salarie; + +public class SalarieCreateDto +{ + public string Nom { get; set; } + public string Prenom { get; set; } + public string TelephoneFixe { get; set; } + public string TelephonePortable { get; set; } + public string Email { get; set; } + public int IdSite { get; set; } + public int IdService { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Salarie/SalarieUpdateDto.cs b/ldap-cesi/DTOs/Inputs/Salarie/SalarieUpdateDto.cs new file mode 100644 index 0000000..3245bfd --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Salarie/SalarieUpdateDto.cs @@ -0,0 +1,20 @@ +namespace ldap_cesi.DTOs.Inputs.Service; + +public class SalarieUpdateDto +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public string Prenom { get; set; } = null!; + + public string TelephoneFixe { get; set; } = null!; + + public string TelephonePortable { get; set; } = null!; + + public string Email { get; set; } = null!; + + public int IdSite { get; set; } + + public int IdService { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Service/ServiceCreateDto.cs b/ldap-cesi/DTOs/Inputs/Service/ServiceCreateDto.cs new file mode 100644 index 0000000..11d3052 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Service/ServiceCreateDto.cs @@ -0,0 +1,6 @@ +namespace ldap_cesi.DTOs.Inputs.Service; + +public class ServiceCreateDto +{ + public string Nom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Service/ServiceUpdateDto.cs b/ldap-cesi/DTOs/Inputs/Service/ServiceUpdateDto.cs new file mode 100644 index 0000000..37e44b5 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Service/ServiceUpdateDto.cs @@ -0,0 +1,8 @@ +namespace ldap_cesi.DTOs.Inputs.Service; + +public class ServiceUpdateDto +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Site/SiteCreateDto.cs b/ldap-cesi/DTOs/Inputs/Site/SiteCreateDto.cs new file mode 100644 index 0000000..706affd --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Site/SiteCreateDto.cs @@ -0,0 +1,6 @@ +namespace ldap_cesi.DTOs.Inputs.Site; + +public class SiteCreateDto +{ + public string Ville { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Site/SiteUpdateDto.cs b/ldap-cesi/DTOs/Inputs/Site/SiteUpdateDto.cs new file mode 100644 index 0000000..bcd5fb7 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Site/SiteUpdateDto.cs @@ -0,0 +1,9 @@ +namespace ldap_cesi.DTOs.Inputs.Site; + +public class SiteUpdateDto +{ + public int Id { get; set; } + + public string Ville { get; set; } + +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Inputs/Utilisateur/UtilisateurLoginDto.cs b/ldap-cesi/DTOs/Inputs/Utilisateur/UtilisateurLoginDto.cs new file mode 100644 index 0000000..2ce64f1 --- /dev/null +++ b/ldap-cesi/DTOs/Inputs/Utilisateur/UtilisateurLoginDto.cs @@ -0,0 +1,7 @@ +namespace ldap_cesi.DTOs.Inputs; + +public class UtilisateurLoginDto +{ + public string Email { get; set; } + public string MotDePasse { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Salarie/SalarieListDto.cs b/ldap-cesi/DTOs/Outputs/Salarie/SalarieListDto.cs new file mode 100644 index 0000000..0615eaa --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Salarie/SalarieListDto.cs @@ -0,0 +1,14 @@ +using ldap_cesi.DTOs.Outputs.Service; + +namespace ldap_cesi.DTOs.Outputs.Salarie; + +public class SalarieListDto +{ + + public int Id { get; set; } + public string Nom { get; set; } + public string Prenom { get; set; } + public string NomComplet => $"{Prenom} {Nom}"; + public ServiceMinimalDto Service { get; set; } + public SiteMinimalDto Site { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Salarie/SalarieMinimalDto.cs b/ldap-cesi/DTOs/Outputs/Salarie/SalarieMinimalDto.cs new file mode 100644 index 0000000..bf4be29 --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Salarie/SalarieMinimalDto.cs @@ -0,0 +1,17 @@ +namespace ldap_cesi.DTOs.Outputs.Salarie; + +public class SalarieMinimalDto +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public string Prenom { get; set; } = null!; + + public string TelephoneFixe { get; set; } = null!; + + public string TelephonePortable { get; set; } = null!; + + public string Email { get; set; } = null!; + +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Salarie/SalarieOutputDetail.cs b/ldap-cesi/DTOs/Outputs/Salarie/SalarieOutputDetail.cs new file mode 100644 index 0000000..f7b1bd3 --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Salarie/SalarieOutputDetail.cs @@ -0,0 +1,21 @@ +using ldap_cesi.DTOs.Outputs.Service; + +namespace ldap_cesi.DTOs.Outputs.Salarie; + +public class SalarieOutputDetail +{ + public int Id { get; set; } + + public string Nom { get; set; } + + public string Prenom { get; set; } + + public string TelephoneFixe { get; set; } + + public string TelephonePortable { get; set; } + + public string Email { get; set; } + + public ServiceMinimalDto Service { get; set; } + public SiteMinimalDto Site { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Service/ServiceMinimalDto.cs b/ldap-cesi/DTOs/Outputs/Service/ServiceMinimalDto.cs new file mode 100644 index 0000000..c373690 --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Service/ServiceMinimalDto.cs @@ -0,0 +1,8 @@ +namespace ldap_cesi.DTOs.Outputs.Service; + +public class ServiceMinimalDto +{ + public int Id { get; set; } + + public string Nom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Site/SiteMinimalDto.cs b/ldap-cesi/DTOs/Outputs/Site/SiteMinimalDto.cs new file mode 100644 index 0000000..eb6cfb4 --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Site/SiteMinimalDto.cs @@ -0,0 +1,8 @@ +namespace ldap_cesi.DTOs.Outputs.Service; + +public class SiteMinimalDto +{ + public int Id { get; set; } + + public string Ville { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/DTOs/Outputs/Utilisateur/UtilisateurOutputDto.cs b/ldap-cesi/DTOs/Outputs/Utilisateur/UtilisateurOutputDto.cs new file mode 100644 index 0000000..b508c8d --- /dev/null +++ b/ldap-cesi/DTOs/Outputs/Utilisateur/UtilisateurOutputDto.cs @@ -0,0 +1,9 @@ +namespace ldap_cesi.DTOs.Outputs.Utilisateur; + +public class UtilisateurOutputDto +{ + public int Id { get; set; } + public string Email { get; set; } + public string Nom { get; set; } + public string RoleNom { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/Entities/Role.cs b/ldap-cesi/Entities/Role.cs new file mode 100644 index 0000000..bd74754 --- /dev/null +++ b/ldap-cesi/Entities/Role.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace ldap_cesi.Entities; + +public partial class Role +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public virtual ICollection Utilisateurs { get; set; } = new List(); +} diff --git a/ldap-cesi/Entities/Salarie.cs b/ldap-cesi/Entities/Salarie.cs new file mode 100644 index 0000000..9ccc76a --- /dev/null +++ b/ldap-cesi/Entities/Salarie.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace ldap_cesi.Entities; + +public partial class Salarie +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public string Prenom { get; set; } = null!; + + public string TelephoneFixe { get; set; } = null!; + + public string TelephonePortable { get; set; } = null!; + + public string Email { get; set; } = null!; + + public int IdSite { get; set; } + + public int IdService { get; set; } + + public virtual Service IdServiceNavigation { get; set; } = null!; + + public virtual Site IdSiteNavigation { get; set; } = null!; +} diff --git a/ldap-cesi/Entities/Service.cs b/ldap-cesi/Entities/Service.cs new file mode 100644 index 0000000..912f597 --- /dev/null +++ b/ldap-cesi/Entities/Service.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace ldap_cesi.Entities; + +public partial class Service +{ + public int Id { get; set; } + + public string Nom { get; set; } = null!; + + public virtual ICollection Salaries { get; set; } = new List(); +} diff --git a/ldap-cesi/Entities/Site.cs b/ldap-cesi/Entities/Site.cs new file mode 100644 index 0000000..1ed7f2a --- /dev/null +++ b/ldap-cesi/Entities/Site.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace ldap_cesi.Entities; + +public partial class Site +{ + public int Id { get; set; } + + public string Ville { get; set; } + + public virtual ICollection Salaries { get; set; } = new List(); +} diff --git a/ldap-cesi/Entities/Utilisateur.cs b/ldap-cesi/Entities/Utilisateur.cs new file mode 100644 index 0000000..9037a5c --- /dev/null +++ b/ldap-cesi/Entities/Utilisateur.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ldap_cesi.Entities; + +public partial class Utilisateur +{ + public int Id { get; set; } + + public string Nom { get; set; } + + public string Prenom { get; set; } + + public string MotDePasse { get; set; } + public string Email { get; set; } + + public int IdRole { get; set; } + [NotMapped] + public string AccessToken { get; set; } + public virtual Role IdRoleNavigation { get; set; } = null!; +} diff --git a/ldap-cesi/Mapper/AutoMapperProfile.cs b/ldap-cesi/Mapper/AutoMapperProfile.cs new file mode 100644 index 0000000..d76b6cb --- /dev/null +++ b/ldap-cesi/Mapper/AutoMapperProfile.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Role; +using ldap_cesi.DTOs.Inputs.Salarie; +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.DTOs.Inputs.Site; +using ldap_cesi.DTOs.Outputs.Salarie; +using ldap_cesi.DTOs.Outputs.Service; +using ldap_cesi.DTOs.Outputs.Utilisateur; +using ldap_cesi.Entities; + +namespace ldap_cesi.Mapper; + +public class AutoMapperProfile : Profile +{ + public AutoMapperProfile() + { + // INPUTS MAPPER + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.TelephoneFixe, opt => opt.MapFrom(src => src.TelephoneFixe)) + .ForMember(dest => dest.TelephonePortable, opt => opt.MapFrom(src => src.TelephonePortable)); + CreateMap() + .ForMember(dest => dest.TelephoneFixe, opt => opt.MapFrom(src => src.TelephoneFixe)) + .ForMember(dest => dest.TelephonePortable, opt => opt.MapFrom(src => src.TelephonePortable)); + + + //OUTPUTS MAPPER + CreateMap() + .ForMember(dest => dest.RoleNom, opt => opt.MapFrom(src => src.IdRoleNavigation.Nom)); + CreateMap() + .ForMember(dest => dest.Service, opt => opt.MapFrom(src => src.IdServiceNavigation)) + .ForMember(dest => dest.Site, opt => opt.MapFrom(src => src.IdSiteNavigation)); + CreateMap() + .ForMember(dest => dest.Service, opt => opt.MapFrom(src => src.IdServiceNavigation)) + .ForMember(dest => dest.Site, opt => opt.MapFrom(src => src.IdSiteNavigation)); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.Salaries, opt => opt.MapFrom(src => src.Salaries)); + CreateMap() + .ForMember(dest => dest.Service, opt => opt.MapFrom(src => src.IdServiceNavigation)) + .ForMember(dest => dest.Site, opt => opt.MapFrom(src => src.IdSiteNavigation)); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.Service, opt => opt.MapFrom(src => src.IdServiceNavigation.Nom)) + .ForMember(dest => dest.Site, opt => opt.MapFrom(src => src.IdSiteNavigation.Ville)); + } +} \ No newline at end of file diff --git a/ldap-cesi/Models/IResponseDataModel.cs b/ldap-cesi/Models/IResponseDataModel.cs new file mode 100644 index 0000000..1eb89dc --- /dev/null +++ b/ldap-cesi/Models/IResponseDataModel.cs @@ -0,0 +1,7 @@ +namespace ldap_cesi.Models; + +public interface IResponseDataModel : IResponseModel +{ + public T Data { get; set; } + string Token { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/Models/IResponseModel.cs b/ldap-cesi/Models/IResponseModel.cs new file mode 100644 index 0000000..1cb04de --- /dev/null +++ b/ldap-cesi/Models/IResponseModel.cs @@ -0,0 +1,8 @@ +namespace ldap_cesi.Models; + +public interface IResponseModel +{ + public int StatusCode { get; set; } + public string? Message { get; set; } + public bool Success { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/Models/PaginatedList.cs b/ldap-cesi/Models/PaginatedList.cs new file mode 100644 index 0000000..cbdb15f --- /dev/null +++ b/ldap-cesi/Models/PaginatedList.cs @@ -0,0 +1,22 @@ +namespace ldap_cesi.Models +{ + public class PaginatedList + { + public List Data { get; } + public int TotalCount { get; } + public int PageNumber { get; } + public int PageSize { get; } + public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); + + public PaginatedList(List data, int totalCount, int pageNumber, int pageSize) + { + Data = data; + TotalCount = totalCount; + PageNumber = pageNumber; + PageSize = pageSize; + } + + public bool HasPreviousPage => PageNumber > 1; + public bool HasNextPage => PageNumber < TotalPages; + } +} \ No newline at end of file diff --git a/ldap-cesi/Models/ResponseDataModel.cs b/ldap-cesi/Models/ResponseDataModel.cs new file mode 100644 index 0000000..973842a --- /dev/null +++ b/ldap-cesi/Models/ResponseDataModel.cs @@ -0,0 +1,10 @@ +namespace ldap_cesi.Models; + +public class ResponseDataModel : ResponseModel, IResponseDataModel where T : class +{ + public T Data { get; set; } = null!; + public int? TotalPages { get; set; } + public int? TotalCount { get; set; } + public int? PageNumber { get; set; } + public int? PageSize { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/Models/ResponseModel.cs b/ldap-cesi/Models/ResponseModel.cs new file mode 100644 index 0000000..2173782 --- /dev/null +++ b/ldap-cesi/Models/ResponseModel.cs @@ -0,0 +1,10 @@ +namespace ldap_cesi.Models; + +public class ResponseModel : IResponseModel +{ + public bool Success { get; set; } + public string? Message { get; set; } + public string? Token { get; set; } + + public int StatusCode { get; set; } +} \ No newline at end of file diff --git a/ldap-cesi/Program.cs b/ldap-cesi/Program.cs new file mode 100644 index 0000000..0974d7a --- /dev/null +++ b/ldap-cesi/Program.cs @@ -0,0 +1,44 @@ +using ldap_cesi.Configurations; +using ldap_cesi.Context; +using ldap_cesi.Seeders; + +var builder = WebApplication.CreateBuilder(args); +builder.BuildConf(); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); + + app.Use(async (context, next) => + { + if (context.Request.Path == "/") + { + context.Response.Redirect("/swagger"); + return; + } + await next(); + }); +} + +using (var scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + var context = services.GetRequiredService(); + var seeder = new Seeders(context); + + if (app.Environment.IsDevelopment()) + { + await seeder.GenerateSalaries(500); + } +} + +app.UseHttpsRedirection(); +app.UseRouting(); +app.UseCors("AllowAll"); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); diff --git a/ldap-cesi/Properties/launchSettings.json b/ldap-cesi/Properties/launchSettings.json new file mode 100644 index 0000000..584f844 --- /dev/null +++ b/ldap-cesi/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:5080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7079;http://localhost:5080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ldap-cesi/Repositories/Interfaces/IRepositoryBase.cs b/ldap-cesi/Repositories/Interfaces/IRepositoryBase.cs new file mode 100644 index 0000000..17e737d --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositoryBase.cs @@ -0,0 +1,29 @@ +using System.Linq.Expressions; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositoryBase where TEntity : class +{ + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task GetByIdAsync(TId id, CancellationToken cancellationToken = default) where TId : notnull; + + Task<(List Data, int TotalPages, int TotalItems)> GetAllAsync(int pageNumber = 1, int pageSize = 10, CancellationToken cancellationToken = default); + Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); + Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default); + Task GetWithRelationsAsync(int id, params Expression>[] relationsAInclude); + + Task<(List Data, int TotalPages, int TotalItems)> GetAllWithRelationsAsync(int pageNumber = 1, int pageSize = 10, params Expression>[] relationInclues); + + Task<(List Data, int TotalPages, int TotalItems)> SearchAsync( + Expression> predicate, + int pageNumber = 1, + int pageSize = 10, + params Expression>[] relationsAInclude); + Task CountRelatedEntitiesAsync(int id, Expression> predicate) where TRelated : class; + + Task FirstOrDefaultAsync(Expression> predicate, + CancellationToken cancellationToken = default); + + Task CountAsync(Expression> predicate, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/Interfaces/IRepositoryRole.cs b/ldap-cesi/Repositories/Interfaces/IRepositoryRole.cs new file mode 100644 index 0000000..e504b27 --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositoryRole.cs @@ -0,0 +1,8 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositoryRole : IRepositoryBase +{ + +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/Interfaces/IRepositorySalarie.cs b/ldap-cesi/Repositories/Interfaces/IRepositorySalarie.cs new file mode 100644 index 0000000..410c326 --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositorySalarie.cs @@ -0,0 +1,11 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositorySalarie : IRepositoryBase +{ + Task GetSalarieWithRelationsAsync(int id); + Task> SearchByNameAsync(string inputRecherche); + Task> GetSalariesBySiteAsync(int siteId); + Task> GetSalariesByServiceAsync(int serviceId); +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/Interfaces/IRepositoryService.cs b/ldap-cesi/Repositories/Interfaces/IRepositoryService.cs new file mode 100644 index 0000000..aba697b --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositoryService.cs @@ -0,0 +1,8 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositoryService : IRepositoryBase +{ + +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/Interfaces/IRepositorySite.cs b/ldap-cesi/Repositories/Interfaces/IRepositorySite.cs new file mode 100644 index 0000000..cc098f9 --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositorySite.cs @@ -0,0 +1,8 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositorySite : IRepositoryBase +{ + +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/Interfaces/IRepositoryUtilisateur.cs b/ldap-cesi/Repositories/Interfaces/IRepositoryUtilisateur.cs new file mode 100644 index 0000000..dcecf23 --- /dev/null +++ b/ldap-cesi/Repositories/Interfaces/IRepositoryUtilisateur.cs @@ -0,0 +1,9 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Repository.Services; + +public interface IRepositoryUtilisateur : IRepositoryBase +{ + Task GetByEmailAsync(string email); + Task GetByIdIncludeRoleAsync(int id); +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/RepositoryBase.cs b/ldap-cesi/Repositories/RepositoryBase.cs new file mode 100644 index 0000000..0b02212 --- /dev/null +++ b/ldap-cesi/Repositories/RepositoryBase.cs @@ -0,0 +1,184 @@ +using System.Linq.Expressions; +using ldap_cesi.Context; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Repository; + +public class RepositoryBase : IRepositoryBase where TEntity : class +{ + protected readonly PgContext _context; + protected readonly DbSet _dbSet; + public RepositoryBase(PgContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _dbSet = context.Set(); + } + public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + try + { + _context.Set().Add(entity); + await SaveChangesAsync(cancellationToken); + return entity; + } + catch (Exception ex) + { + throw new Exception("Erreur pendant l'ajout de l'entité.", ex); + } + } + + public virtual async Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return await _context.Set().AnyAsync(predicate, cancellationToken); + } + + public virtual async Task GetByIdAsync(TId id, CancellationToken cancellationToken = default) where TId : notnull + { + try + { + return await _context.FindAsync(id, cancellationToken); + } + catch (Exception ex) + { + throw new Exception($"Erreur lors de la récupération avec l'id : {id}.", ex); + } + } + + + public virtual async Task<(List Data, int TotalPages, int TotalItems)> GetAllAsync(int pageNumber = 1, int pageSize = 10, CancellationToken cancellationToken = default) + { + try + { + var totalCount = await _dbSet.CountAsync(cancellationToken); + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + var data = await _dbSet + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (data, totalPages, totalCount); + } + catch (Exception ex) + { + throw new Exception("Erreur pendant la récupération des entités.", ex); + } + } + + public virtual async Task FirstOrDefaultAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return await _context.Set().FirstOrDefaultAsync(predicate, cancellationToken); + } + + public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + try + { + _context.Set().Update(entity); + await SaveChangesAsync(cancellationToken); + return true; + } + catch (Exception ex) + { + throw new Exception("Erreur pendant la mis à jour", ex); + } + } + + public virtual async Task<(List Data, int TotalPages, int TotalItems)> GetAllWithRelationsAsync(int pageNumber = 1, int pageSize = 10, params Expression>[] relationInclues) + { + IQueryable query = _dbSet; + + foreach (var relationInclue in relationInclues) + { + query = query.Include(relationInclue); + } + + var totalCount = await query.CountAsync(); + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + var data = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (data, totalPages, totalCount); + } + + public virtual async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) + { + try + { + _context.Set().Remove(entity); + await SaveChangesAsync(cancellationToken); + return true; + } + catch (Exception ex) + { + throw new Exception("Erreur pendant la suppression de l'entité", ex); + } + } + + protected async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + try + { + return await _context.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) + { + throw new Exception("Erreur pendant le sauvegarde en base de donnése.", ex); + } + } + + + public virtual async Task GetWithRelationsAsync(int id, params Expression>[] relationInclues) + { + IQueryable query = _dbSet; + + foreach (var relationInclue in relationInclues) + { + query = query.Include(relationInclue); + } + + return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id); + } + + public virtual async Task<(List Data, int TotalPages, int TotalItems)> SearchAsync( + Expression> predicate, + int pageNumber = 1, + int pageSize = 10, + params Expression>[] relationsAInclude) + { + IQueryable query = _dbSet; + + foreach (var relationInclue in relationsAInclude) + { + query = query.Include(relationInclue); + } + + query = query.Where(predicate); + + var totalCount = await query.CountAsync(); + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + var data = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (data, totalPages, totalCount); + } + + public virtual async Task CountAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return await _dbSet.CountAsync(predicate, cancellationToken); + } + + public virtual async Task CountRelatedEntitiesAsync(int id, Expression> predicate) where TRelated : class + { + return await _context.Set().CountAsync(predicate); + } + +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/RoleRepository.cs b/ldap-cesi/Repositories/RoleRepository.cs new file mode 100644 index 0000000..2d7ee07 --- /dev/null +++ b/ldap-cesi/Repositories/RoleRepository.cs @@ -0,0 +1,13 @@ +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Repository.Services; + +namespace ldap_cesi.Repository; + +public class RoleRepository : RepositoryBase, IRepositoryRole +{ + public RoleRepository(PgContext context) : base(context) + { + + } +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/SalarieRepository.cs b/ldap-cesi/Repositories/SalarieRepository.cs new file mode 100644 index 0000000..cb5d329 --- /dev/null +++ b/ldap-cesi/Repositories/SalarieRepository.cs @@ -0,0 +1,49 @@ +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Repository.Services; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Repository; + +public class SalarieRepository : RepositoryBase, IRepositorySalarie +{ + public SalarieRepository(PgContext context) : base(context) + { + + } + + public async Task GetSalarieWithRelationsAsync(int id) + { + return await _context.Salaries + .Include(s => s.IdServiceNavigation) + .Include(s => s.IdSiteNavigation) + .FirstOrDefaultAsync(s => s.Id == id); + } + + public async Task> SearchByNameAsync(string inputRecherche) + { + return await _context.Salaries + .Where(s => s.Nom.Contains(inputRecherche) || s.Prenom.Contains(inputRecherche)) + .Include(s => s.IdServiceNavigation) + .Include(s => s.IdSiteNavigation) + .ToListAsync(); + } + + public async Task> GetSalariesBySiteAsync(int siteId) + { + return await _context.Salaries + .Where(s => s.IdSite == siteId) + .Include(s => s.IdServiceNavigation) + .Include(s => s.IdSiteNavigation) + .ToListAsync(); + } + + public async Task> GetSalariesByServiceAsync(int serviceId) + { + return await _context.Salaries + .Where(s => s.IdService == serviceId) + .Include(s => s.IdServiceNavigation) + .Include(s => s.IdSiteNavigation) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/ServiceRepository.cs b/ldap-cesi/Repositories/ServiceRepository.cs new file mode 100644 index 0000000..e0f15e8 --- /dev/null +++ b/ldap-cesi/Repositories/ServiceRepository.cs @@ -0,0 +1,13 @@ +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Repository.Services; + +namespace ldap_cesi.Repository; + +public class ServiceRepository : RepositoryBase, IRepositoryService +{ + public ServiceRepository(PgContext context) : base(context) + { + + } +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/SiteRepository.cs b/ldap-cesi/Repositories/SiteRepository.cs new file mode 100644 index 0000000..3b522b8 --- /dev/null +++ b/ldap-cesi/Repositories/SiteRepository.cs @@ -0,0 +1,13 @@ +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Repository.Services; + +namespace ldap_cesi.Repository; + +public class SiteRepository : RepositoryBase, IRepositorySite +{ + public SiteRepository(PgContext context) : base(context) + { + + } +} \ No newline at end of file diff --git a/ldap-cesi/Repositories/UtilisateurRepository.cs b/ldap-cesi/Repositories/UtilisateurRepository.cs new file mode 100644 index 0000000..a86435c --- /dev/null +++ b/ldap-cesi/Repositories/UtilisateurRepository.cs @@ -0,0 +1,27 @@ +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Repository.Services; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Repository; + +public class UtilisateurRepository : RepositoryBase, IRepositoryUtilisateur +{ + public UtilisateurRepository(PgContext context) : base(context) + { + + } + + public async Task GetByEmailAsync(string email) + { + return await _context.Utilisateurs + .Include(u => u.IdRoleNavigation) + .FirstOrDefaultAsync(u => u.Email == email); + } + public async Task GetByIdIncludeRoleAsync(int id) + { + return await _context.Utilisateurs + .Include(u => u.IdRoleNavigation) + .FirstOrDefaultAsync(u => u.Id == id); + } +} \ No newline at end of file diff --git a/ldap-cesi/Seeders/Seeders.cs b/ldap-cesi/Seeders/Seeders.cs new file mode 100644 index 0000000..27334e3 --- /dev/null +++ b/ldap-cesi/Seeders/Seeders.cs @@ -0,0 +1,49 @@ +using Bogus; +using ldap_cesi.Context; +using ldap_cesi.Entities; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Seeders; + +public class Seeders +{ + private readonly PgContext _context; + + public Seeders(PgContext context) + { + _context = context; + } + + public async Task GenerateSalaries(int count = 1000) + { + + if (await _context.Salaries.AnyAsync()) + { + Console.WriteLine("Des salariés existent déjà dans la base de données. Aucune donnée insérée."); + return; + } + + var salarieFaker = new Faker("fr") + .RuleFor(s => s.Nom, f => f.Name.LastName()) + .RuleFor(s => s.Prenom, f => f.Name.FirstName()) + .RuleFor(s => s.TelephoneFixe, f => f.Phone.PhoneNumber()) + .RuleFor(s => s.TelephonePortable, f => f.Phone.PhoneNumber()) + .RuleFor(s => s.Email, (f, s) => f.Internet.Email(s.Prenom, s.Nom)); + + var salaries = salarieFaker.Generate(count); + + // on s'assure que les relations (Service, Site) existent avant d'ajouter les salariés + var services = await _context.Services.ToListAsync(); + var sites = await _context.Sites.ToListAsync(); + + var random = new Random(); + foreach (var salarie in salaries) + { + salarie.IdServiceNavigation = services[random.Next(services.Count)]; + salarie.IdSiteNavigation = sites[random.Next(sites.Count)]; + } + + await _context.Salaries.AddRangeAsync(salaries); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IJwtService.cs b/ldap-cesi/Services/Interfaces/IJwtService.cs new file mode 100644 index 0000000..fcfbc88 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IJwtService.cs @@ -0,0 +1,11 @@ +using ldap_cesi.Entities; + +namespace ldap_cesi.Services.Interfaces; + +public interface IJwtService +{ + string GenerateToken(Utilisateur utilisateur); + string GetPublicKey(); + Task ValidateToken(string token, int userId); + Task InvalidateToken(string token); +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IRoleService.cs b/ldap-cesi/Services/Interfaces/IRoleService.cs new file mode 100644 index 0000000..41e1c2a --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IRoleService.cs @@ -0,0 +1,10 @@ +using ldap_cesi.Entities; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Role; + +namespace ldap_cesi.Services.Interfaces +{ + public interface IRoleService : IServiceBase + { + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IRsaKeyService.cs b/ldap-cesi/Services/Interfaces/IRsaKeyService.cs new file mode 100644 index 0000000..a08d475 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IRsaKeyService.cs @@ -0,0 +1,8 @@ +using System.Security.Cryptography; + +namespace ldap_cesi.Services.Interfaces; + +public interface IRsaKeyService +{ + RSA GetRsaKey(); +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/ISalarieService.cs b/ldap-cesi/Services/Interfaces/ISalarieService.cs new file mode 100644 index 0000000..8386361 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/ISalarieService.cs @@ -0,0 +1,17 @@ +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Salarie; +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.DTOs.Outputs.Salarie; +using ldap_cesi.Entities; +using ldap_cesi.Models; + +namespace ldap_cesi.Services.Interfaces; + +public interface ISalarieService : IServiceBase +{ + Task>> GetSalariesByService(int serviceId, int pageNumber = 1, + int pageSize = 25); + + Task>> GetSalariesBySite(int siteId, int pageNumber = 1, + int pageSize = 25); +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IServiceBase.cs b/ldap-cesi/Services/Interfaces/IServiceBase.cs new file mode 100644 index 0000000..f11aef2 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IServiceBase.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; +using ldap_cesi.Models; + +namespace ldap_cesi.Services.Interfaces; + +public interface IServiceBase + where T : class + where TDto : class + where TCreateDto : class + where TUpdateDto : class +{ + Task>> GetAll(int pageNumber, int pageSize); + Task> GetById(int id); + Task> GetByIdWithRelations(int id, params Expression>[] relationsAInclures); // préciser avec une ou des fonctions lambda les relations à inclure dans la réponse + Task>> GetAllWithRelationsAsync(int pageNumber, int pageSize, params Expression>[] relationsAInclure); + + Task>> SearchWithRelations(string searchTerm, int pageNumber, int pageSize, + params Expression>[] includeProperties); + Task> Create(TCreateDto dto); + Task> Update(TUpdateDto dto); + Task> Delete(int id); + Task> DeleteWithDependencyCheck(int id, Expression> relationPredicate, string relationErrorMessage) where TRelated : class; +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IServiceService.cs b/ldap-cesi/Services/Interfaces/IServiceService.cs new file mode 100644 index 0000000..d509e38 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IServiceService.cs @@ -0,0 +1,12 @@ +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.DTOs; +using ldap_cesi.Entities; +using ldap_cesi.Models; + +namespace ldap_cesi.Services.Interfaces +{ + public interface IServiceService : IServiceBase + { + Task> DeleteWithEntiteCheck(int id); + } +} diff --git a/ldap-cesi/Services/Interfaces/ISiteService.cs b/ldap-cesi/Services/Interfaces/ISiteService.cs new file mode 100644 index 0000000..08e3b93 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/ISiteService.cs @@ -0,0 +1,11 @@ +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Site; +using ldap_cesi.Entities; +using ldap_cesi.Models; + +namespace ldap_cesi.Services.Interfaces; + +public interface ISiteService : IServiceBase +{ + Task> DeleteWithEntiteCheck(int id); +} \ No newline at end of file diff --git a/ldap-cesi/Services/Interfaces/IUtilisateurService.cs b/ldap-cesi/Services/Interfaces/IUtilisateurService.cs new file mode 100644 index 0000000..88d9fa2 --- /dev/null +++ b/ldap-cesi/Services/Interfaces/IUtilisateurService.cs @@ -0,0 +1,13 @@ +using ldap_cesi.DTOs.Inputs; +using ldap_cesi.DTOs.Outputs.Utilisateur; +using ldap_cesi.Entities; +using ldap_cesi.Models; + +namespace ldap_cesi.Services.Interfaces; + +public interface IUtilisateurService +{ + Task>> GetAll(); + Task> GetById(int id); + Task> Login(UtilisateurLoginDto utilisateurInput); +} \ No newline at end of file diff --git a/ldap-cesi/Services/JwtService.cs b/ldap-cesi/Services/JwtService.cs new file mode 100644 index 0000000..8464c4b --- /dev/null +++ b/ldap-cesi/Services/JwtService.cs @@ -0,0 +1,167 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using ldap_cesi.Context; +using ldap_cesi.Entities; +using ldap_cesi.Services.Interfaces; +using Microsoft.IdentityModel.Tokens; + +namespace ldap_cesi.Services; + +public class JwtService : IJwtService +{ + private readonly IRsaKeyService _rsaKeyService; + private readonly IConfiguration _configuration; + private readonly PgContext _context; + private readonly ILogger _logger; + + public JwtService( + IConfiguration configuration, + PgContext context, + IRsaKeyService rsaKeyService, + ILogger logger) + { + _configuration = configuration; + _context = context; + _rsaKeyService = rsaKeyService; + _logger = logger; + } + + public string GenerateToken(Utilisateur utilisateur) + { + try + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, utilisateur.Email), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.Name, utilisateur.Nom), + new Claim(ClaimTypes.Role, utilisateur.IdRoleNavigation.Nom), + new Claim(ClaimTypes.NameIdentifier, utilisateur.Id.ToString()) + }; + + var key = new RsaSecurityKey(_rsaKeyService.GetRsaKey()); + var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256); + + var tokenExpiryMinutes = _configuration.GetValue("Jwt:TokenExpiryMinutes", 30); + var token = new JwtSecurityToken( + issuer: _configuration["Jwt:Issuer"], + audience: _configuration["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddMinutes(tokenExpiryMinutes), + signingCredentials: creds); + + var tokenString = new JwtSecurityTokenHandler().WriteToken(token); + utilisateur.AccessToken = tokenString; + _context.Utilisateurs.Update(utilisateur); + _context.SaveChanges(); + + return tokenString; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produitel lors de la génération du token."); + throw; + } + } + + public string GetPublicKey() + { + try + { + var publicKey = _rsaKeyService.GetRsaKey().ExportSubjectPublicKeyInfo(); + return Convert.ToBase64String(publicKey); + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite pendant la récupération de la clé."); + throw; + } + } + + // Ajouter cette méthode à votre JwtService.cs + public async Task InvalidateToken(string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + + // récupération du token + var jwtToken = tokenHandler.ReadJwtToken(token); + + // identifiant de l'utilisateur + var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + + if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId)) + { + _logger.LogWarning("Erreur d'invalidation du token : Id utilisateur non trouvé ou invalide."); + return false; + } + + var utilisateur = await _context.Utilisateurs.FindAsync(userId); + if (utilisateur == null) + { + _logger.LogWarning("Erreur d'invalidation du token : Utilisateur non trouvé."); + return false; + } + + // delte le token stocké + utilisateur.AccessToken = null; + _context.Utilisateurs.Update(utilisateur); + await _context.SaveChangesAsync(); + + _logger.LogInformation($"Token invalidé pour l'utilisateur {userId}."); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite pendant l'invalidation du token JWT."); + return false; + } + } + + public async Task ValidateToken(string token, int userId) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new RsaSecurityKey(_rsaKeyService.GetRsaKey()), + ValidateIssuer = true, + ValidateAudience = true, + ValidIssuer = _configuration["Jwt:Issuer"], + ValidAudience = _configuration["Jwt:Audience"], + ClockSkew = TimeSpan.Zero, + ValidateLifetime = true + }; + + var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); + var nameIdClaim = principal.FindFirst(ClaimTypes.NameIdentifier); + if (nameIdClaim == null || !int.TryParse(nameIdClaim.Value, out var tokenUserId) || tokenUserId != userId) + { + _logger.LogWarning("Erreur de validation : Id utilisateur invalide."); + return false; + } + + var utilisateur = await _context.Utilisateurs.FindAsync(userId); + if (utilisateur == null) + { + _logger.LogWarning("Erreur de validation : Utilisateur non trouvé."); + return false; + } + + utilisateur.AccessToken = token; + _context.Utilisateurs.Update(utilisateur); + await _context.SaveChangesAsync(); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors pendant la validation du token JWT."); + return false; + } + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/RoleService.cs b/ldap-cesi/Services/RoleService.cs new file mode 100644 index 0000000..c6e44df --- /dev/null +++ b/ldap-cesi/Services/RoleService.cs @@ -0,0 +1,17 @@ +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using ldap_cesi.Entities; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Role; +using AutoMapper; +using ldap_cesi.Validator.Role; + +namespace ldap_cesi.Services; + +public class RoleService : ServiceBase,IRoleService +{ + public RoleService(IRepositoryRole repositoryRole, IMapper mapper, ILogger logger, RoleCreateValidator roleCreateValidator, RoleUpdateValidator roleUpdateValidator) + : base(repositoryRole, mapper, logger, roleCreateValidator, roleUpdateValidator) + { + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/RsaKeyService.cs b/ldap-cesi/Services/RsaKeyService.cs new file mode 100644 index 0000000..a71da2f --- /dev/null +++ b/ldap-cesi/Services/RsaKeyService.cs @@ -0,0 +1,36 @@ +using System.Security.Cryptography; +using ldap_cesi.Services.Interfaces; + +namespace ldap_cesi.Services; + +public class RsaKeyService : IRsaKeyService +{ + private readonly RSA _rsa; + private readonly string _keyPath; + + public RsaKeyService(IConfiguration configuration) + { + _rsa = RSA.Create(); + _keyPath = configuration["Jwt:KeyPath"] ?? "cle.bin"; + LoadOrCreateRsaKey(); + } + + private void LoadOrCreateRsaKey() + { + if (File.Exists(_keyPath)) + { + var keyBytes = File.ReadAllBytes(_keyPath); + _rsa.ImportRSAPrivateKey(keyBytes, out _); + } + else + { + var keyBytes = _rsa.ExportRSAPrivateKey(); + File.WriteAllBytes(_keyPath, keyBytes); + } + } + + public RSA GetRsaKey() + { + return _rsa; + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/SalarieService.cs b/ldap-cesi/Services/SalarieService.cs new file mode 100644 index 0000000..c339300 --- /dev/null +++ b/ldap-cesi/Services/SalarieService.cs @@ -0,0 +1,98 @@ +using AutoMapper; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Salarie; +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.DTOs.Outputs.Salarie; +using ldap_cesi.Entities; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using ldap_cesi.Validator.Salarie; + +namespace ldap_cesi.Services; + +public class SalarieService : ServiceBase, ISalarieService +{ + private IRepositorySalarie _repositorySalarie; + private readonly IRepositorySite _repositorySite; + private readonly IRepositoryService _repositoryService; + private readonly IMapper _mapper; + + public SalarieService(IRepositorySalarie repositorySalarie, IMapper mapper, IRepositorySite repositorySite, IRepositoryService repositoryService, + ILogger logger, SalarieCreateValidator salarieCreateValidator, SalarieUpdateValidator salarieUpdateValidator) + : base(repositorySalarie, mapper, logger, salarieCreateValidator, salarieUpdateValidator) + { + _repositorySalarie = repositorySalarie; + _repositorySite = repositorySite; + _repositoryService = repositoryService; + _mapper = mapper; + } + + public async Task>> GetSalariesBySite(int siteId, int pageNumber = 1, int pageSize = 25) + { + var site = await _repositorySite.GetByIdAsync(siteId); + if (site == null) + { + return new ResponseDataModel> + { + Success = false, + Message = "Site non trouvé", + StatusCode = 404 + }; + } + + var salaries = await _repositorySalarie.GetSalariesBySiteAsync(siteId); + var totalCount = salaries.Count; + var paginatedSalaries = salaries + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToList(); + + var salariesDto = _mapper.Map>(paginatedSalaries); + + return new ResponseDataModel> + { + Success = true, + Data = salariesDto, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize), + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize, + StatusCode = 200 + }; + } + + public async Task>> GetSalariesByService(int serviceId, int pageNumber = 1, int pageSize = 25) + { + var service = await _repositoryService.GetByIdAsync(serviceId); + if (service == null) + { + return new ResponseDataModel> + { + Success = false, + Message = "Service non trouvé", + StatusCode = 404 + }; + } + + var salaries = await _repositorySalarie.GetSalariesByServiceAsync(serviceId); + var totalCount = salaries.Count; + var paginatedSalaries = salaries + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToList(); + + var salariesDto = _mapper.Map>(paginatedSalaries); + + return new ResponseDataModel> + { + Success = true, + Data = salariesDto, // Les salariés paginés + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize), + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize, + StatusCode = 200 + }; + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/ServiceBase.cs b/ldap-cesi/Services/ServiceBase.cs new file mode 100644 index 0000000..a8a72c1 --- /dev/null +++ b/ldap-cesi/Services/ServiceBase.cs @@ -0,0 +1,360 @@ +using AutoMapper; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using System.Linq.Expressions; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace ldap_cesi.Services; + +public class ServiceBase : IServiceBase + where T : class + where TDto : class + where TCreateDto : class + where TUpdateDto : class +{ + protected readonly IRepositoryBase _repository; + protected readonly IMapper _mapper; + protected readonly ILogger> _logger; + protected readonly IValidator _createDtoValidator; + protected readonly IValidator _updateDtoValidator; + + public ServiceBase(IRepositoryBase repository, IMapper mapper, ILogger> logger, IValidator createDtoValidator, IValidator updateDtoValidator) + { + _repository = repository; + _mapper = mapper; + _logger = logger; + _createDtoValidator = createDtoValidator; + _updateDtoValidator = updateDtoValidator; + } + + private Expression> BuildSearchPredicate(string inputSearch) + { + return entity => + EF.Functions.ILike(EF.Property(entity, "Nom"), $"%{inputSearch}%") || + EF.Functions.ILike(EF.Property(entity, "Prenom"), $"%{inputSearch}%"); + } + public virtual async Task>> GetAll(int pageNumber, int pageSize) + { + try + { + var response = await _repository.GetAllAsync( pageNumber, pageSize); + return new ResponseDataModel> + { + Success = true, + Data = response.Data, + TotalPages = response.TotalPages, + TotalCount = response.TotalItems, + StatusCode = 200, + Message = "Liste des entités récupérée avec succès." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors de la récupération des entités."); + return new ResponseDataModel> + { + Success = false, + Message = "Une erreur s'est produite lors de la récupération des entités.", + StatusCode = 500 + }; + } + } + + public virtual async Task> GetById(int id) + { + try + { + var entity = await _repository.GetByIdAsync(id); + if (entity == null) + { + return new ResponseDataModel + { + Success = false, + Message = $"Aucune entité trouvée avec l'identifiant {id}.", + StatusCode = 404 + }; + } + + return new ResponseDataModel + { + Success = true, + Data = entity, + StatusCode = 200, + Message = "Entité récupérée avec succès." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Une erreur s'est produite lors de la récupération de l'entité avec l'identifiant {id}."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la récupération de l'entité.", + StatusCode = 500 + }; + } + } + + public virtual async Task>> GetAllWithRelationsAsync(int pageNumber, int pageSize,params Expression>[] relationsAInclure) + { + try + { + var response = await _repository.GetAllWithRelationsAsync(pageNumber, pageSize,relationsAInclure); + var dtos = _mapper.Map>(response.Data); + return new ResponseDataModel> + { + Success = true, + Data = dtos, + TotalPages = response.TotalPages, + TotalCount = response.TotalItems, + StatusCode = 200, + Message = "Liste des entités récupérée avec succès." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors de la récupération des entités."); + return new ResponseDataModel> + { + Success = false, + Message = "Une erreur s'est produite lors de la récupération des entités.", + StatusCode = 500 + }; + } + } + + public virtual async Task> GetByIdWithRelations(int id, params Expression>[] relationsAInclure) + { + try + { + var entity = await _repository.GetWithRelationsAsync(id, relationsAInclure); + if (entity == null) + { + return new ResponseDataModel + { + Success = false, + Message = $"Aucune entité trouvée avec l'identifiant {id}.", + StatusCode = 404 + }; + } + + var dto = _mapper.Map(entity); + return new ResponseDataModel + { + Success = true, + Data = dto, + StatusCode = 200, + Message = "Entité avec relations récupérée avec succès." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Une erreur s'est produite lors de la récupération de l'entité avec l'identifiant {id}."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la récupération de l'entité avec relations.", + StatusCode = 500 + }; + } + } + + public virtual async Task> Create(TCreateDto dto) + { + try + { + var validationResult = _createDtoValidator.Validate(dto); + if (!validationResult.IsValid) + { + return new ResponseDataModel + { + Success = false, + Message = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)), + StatusCode = 400 + }; + } + var entity = _mapper.Map(dto); + var createdEntity = await _repository.AddAsync(entity); + return new ResponseDataModel + { + Success = createdEntity != null, + Data = createdEntity, + StatusCode = createdEntity != null ? 201 : 500, + Message = createdEntity != null ? "Entité créée avec succès." : "Échec de la création de l'entité." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors de la création de l'entité."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la création de l'entité.", + StatusCode = 500 + }; + } + } + + public virtual async Task> Update(TUpdateDto dto) + { + try + { + var validationResult = _updateDtoValidator.Validate(dto); + if (!validationResult.IsValid) + { + return new ResponseDataModel + { + Success = false, + Message = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)), + StatusCode = 400 + }; + } + var entity = _mapper.Map(dto); + bool isUpdated = await _repository.UpdateAsync(entity); + return new ResponseDataModel + { + Success = isUpdated, + Data = isUpdated ? entity : default, + StatusCode = isUpdated ? 200 : 500, + Message = isUpdated ? "Entité mise à jour avec succès." : "Échec de la mise à jour de l'entité." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors de la mise à jour de l'entité."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la mise à jour de l'entité.", + StatusCode = 500 + }; + } + } + + public virtual async Task> Delete(int id) + { + try + { + var entity = await _repository.GetByIdAsync(id); + if (entity == null) + { + return new ResponseDataModel + { + Success = false, + Message = $"Aucune entité trouvée avec l'identifiant {id}.", + StatusCode = 404 + }; + } + + var isDeleted = await _repository.DeleteAsync(entity); + return new ResponseDataModel + { + Success = isDeleted, + Data = isDeleted ? id.ToString() : null, + StatusCode = isDeleted ? 200 : 500, + Message = isDeleted ? "Entité supprimée avec succès." : "Échec de la suppression de l'entité." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Une erreur s'est produite lors de la suppression de l'entité avec l'identifiant {id}."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la suppression de l'entité.", + StatusCode = 500 + }; + } + } + + public virtual async Task>> SearchWithRelations(string searchTerm, int pageNumber, int pageSize, params Expression>[] includeProperties) + { + if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length < 2) + { + return new ResponseDataModel> + { + Success = false, + Message = "Le terme de recherche doit contenir au moins deux caractères.", + StatusCode = 400 + }; + } + + try + { + var predicate = BuildSearchPredicate(searchTerm); + var response = await _repository.SearchAsync(predicate, pageNumber, pageSize, includeProperties); + var dtos = _mapper.Map>(response.Data); + + return new ResponseDataModel> + { + Success = true, + Data = dtos, + TotalPages = response.TotalPages, + TotalCount = response.TotalItems, + StatusCode = 200, + Message = "Recherche effectuée avec succès." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Une erreur s'est produite lors de la recherche des entités."); + return new ResponseDataModel> + { + Success = false, + Message = "Une erreur s'est produite lors de la recherche des entités.", + StatusCode = 500 + }; + } + } + + public virtual async Task> DeleteWithDependencyCheck(int id, Expression> relationPredicate, string relationErrorMessage) where TRelated : class + { + try + { + var entity = await _repository.GetByIdAsync(id); + if (entity == null) + { + return new ResponseDataModel + { + Success = false, + Message = $"Aucune entité trouvée avec l'identifiant {id}.", + StatusCode = 404 + }; + } + + // Vérifier si des entités dépendantes existent + var relatedCount = await (_repository as IRepositoryBase).CountRelatedEntitiesAsync(id, relationPredicate); + if (relatedCount > 0) + { + return new ResponseDataModel + { + Success = false, + Message = relationErrorMessage, + StatusCode = 400 + }; + } + + var isDeleted = await _repository.DeleteAsync(entity); + return new ResponseDataModel + { + Success = isDeleted, + Data = isDeleted ? id.ToString() : null, + StatusCode = isDeleted ? 200 : 500, + Message = isDeleted ? "Entité supprimée avec succès." : "Échec de la suppression de l'entité." + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Une erreur s'est produite lors de la suppression de l'entité avec l'identifiant {id}."); + return new ResponseDataModel + { + Success = false, + Message = "Une erreur s'est produite lors de la suppression de l'entité.", + StatusCode = 500 + }; + } + } + +} \ No newline at end of file diff --git a/ldap-cesi/Services/ServiceService.cs b/ldap-cesi/Services/ServiceService.cs new file mode 100644 index 0000000..fd8de3a --- /dev/null +++ b/ldap-cesi/Services/ServiceService.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using ldap_cesi.DTOs.Inputs.Service; +using ldap_cesi.Entities; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using ldap_cesi.Validator.Service; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Service; + +namespace ldap_cesi.Services; + +public class ServiceService : ServiceBase, IServiceService +{ + + public ServiceService(IRepositoryService repositoryService, IMapper mapper, ILogger logger, ServiceCreateValidator serviceCreateValidator, ServiceUpdateValidator serviceUpdateValidator) + : base(repositoryService, mapper, logger, serviceCreateValidator, serviceUpdateValidator) + { + } + public async Task> DeleteWithEntiteCheck(int id) + { + return await DeleteWithDependencyCheck( + id, + salarie => salarie.IdServiceNavigation.Id == id, + "il n'est pas possible de supprimer ce service car des salariés y sont liés" + ); + } +} \ No newline at end of file diff --git a/ldap-cesi/Services/SiteService.cs b/ldap-cesi/Services/SiteService.cs new file mode 100644 index 0000000..f5c99dc --- /dev/null +++ b/ldap-cesi/Services/SiteService.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using ldap_cesi.DTOs; +using ldap_cesi.DTOs.Inputs.Site; +using ldap_cesi.Entities; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using ldap_cesi.Validator.Site; + +namespace ldap_cesi.Services; + +public class SiteService : ServiceBase, ISiteService +{ + + private readonly IRepositorySite _repositorySite; + + public SiteService(IRepositorySite repositorySite, IMapper mapper, + ILogger logger, SiteCreateValidator siteCreateValidator, SiteUpdateValidator siteUpdateValidator) + : base(repositorySite, mapper, logger, siteCreateValidator, siteUpdateValidator) + { + _repositorySite = repositorySite; + } + + public async Task> DeleteWithEntiteCheck(int id) + { + return await DeleteWithDependencyCheck( + id, + salarie => salarie.IdSiteNavigation.Id == id, + "il n'est pas possible de supprimer ce site car des salariés y sont liés" + ); + } +} diff --git a/ldap-cesi/Services/UtilisateurService.cs b/ldap-cesi/Services/UtilisateurService.cs new file mode 100644 index 0000000..d18ff73 --- /dev/null +++ b/ldap-cesi/Services/UtilisateurService.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using ldap_cesi.DTOs.Inputs; +using ldap_cesi.DTOs.Outputs.Utilisateur; +using ldap_cesi.Models; +using ldap_cesi.Repository.Services; +using ldap_cesi.Services.Interfaces; +using ldap_cesi.Validator.Utilisateur; + +namespace ldap_cesi.Services; + +public class UtilisateurService : IUtilisateurService +{ + private readonly IRepositoryUtilisateur _repositoryUtilisateur; + private readonly IJwtService _jwtService; + private readonly IMapper _mapper; + public UtilisateurService(IRepositoryUtilisateur repositoryUtilisateur, IJwtService jwtService, IMapper mapper) + { + _repositoryUtilisateur = repositoryUtilisateur; + _jwtService = jwtService; + _mapper = mapper; + } + + public async Task>> GetAll() + { + var utilisateurs = await _repositoryUtilisateur.GetAllAsync(1,10); + var utilisateursOutputDto = _mapper.Map>(utilisateurs); + return new ResponseDataModel> + { + Success = true, + StatusCode = 200, + Data = utilisateursOutputDto, + }; + } + + public async Task> GetById(int id) + { + var utililisateur = await _repositoryUtilisateur.GetByIdIncludeRoleAsync(id); + var utilisateurOutput = _mapper.Map(utililisateur); + return new ResponseDataModel + { + Success = true, + StatusCode = 200, + Data = utilisateurOutput, + }; + } + + public async Task> Login(UtilisateurLoginDto utilisateurInput) + { + var validation = new UtilisateurLoginValidator(); + var result = validation.Validate(utilisateurInput); + if (!result.IsValid) + { + return new ResponseDataModel + { + StatusCode = 400, + Success = false, + Message = "Données utilisateur invalides: " + string.Join(", ", result.Errors) + }; + } + + var utilisateur = await _repositoryUtilisateur.GetByEmailAsync(utilisateurInput.Email); + + if (utilisateur == null) + { + return new ResponseDataModel + { + Success = false, + StatusCode = 404, + Message = "Utilisateur non trouvé." + }; + } + + if (!BCrypt.Net.BCrypt.Verify(utilisateurInput.MotDePasse, utilisateur.MotDePasse)) + { + return new ResponseDataModel + { + Success = false, + StatusCode = 401, + Message = "Mot de passe incorrect." + }; + } + var token = _jwtService.GenerateToken(utilisateur); + var utilisateurOutputDto = _mapper.Map(utilisateur); + + return new ResponseDataModel + { + Success = true, + StatusCode = 200, + Data = utilisateurOutputDto, + Token = token, + Message = "Connexion réussie." + }; + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Role/RoleCreateValidator.cs b/ldap-cesi/Validator/Role/RoleCreateValidator.cs new file mode 100644 index 0000000..ff2e572 --- /dev/null +++ b/ldap-cesi/Validator/Role/RoleCreateValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Role; + +namespace ldap_cesi.Validator.Role; + +public class RoleCreateValidator : AbstractValidator +{ + public RoleCreateValidator() + { + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Role/RoleUpdateValidator.cs b/ldap-cesi/Validator/Role/RoleUpdateValidator.cs new file mode 100644 index 0000000..1bdec9d --- /dev/null +++ b/ldap-cesi/Validator/Role/RoleUpdateValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Role; + +namespace ldap_cesi.Validator.Role; + +public class RoleUpdateValidator : AbstractValidator +{ + public RoleUpdateValidator() + { + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + RuleFor(x => x.Id) + .NotEmpty().WithMessage("L'identifiant du Rôle est requis."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Salarie/SalarieCreateValidator.cs b/ldap-cesi/Validator/Salarie/SalarieCreateValidator.cs new file mode 100644 index 0000000..d07721f --- /dev/null +++ b/ldap-cesi/Validator/Salarie/SalarieCreateValidator.cs @@ -0,0 +1,41 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Salarie; + +namespace ldap_cesi.Validator.Salarie; + +public class SalarieCreateValidator : AbstractValidator +{ + public SalarieCreateValidator() + { + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.Prenom) + .NotEmpty().WithMessage("Le prénom est requis.") + .MaximumLength(50).WithMessage("Le prénom ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.TelephoneFixe) + .NotEmpty().WithMessage("Le téléphone fixe est requis.") + .Matches(@"^(\+33|0)[1-9](\d{2}){4}$") + .WithMessage("Le numéro de téléphone fixe n'est pas valide. Format attendu : +33XXXXXXXXX ou 0XXXXXXXXX.") + .MaximumLength(15).WithMessage("Le téléphone fixe ne doit pas dépasser 15 caractères."); + + RuleFor(x => x.TelephonePortable) + .NotEmpty().WithMessage("Le téléphone portable est requis.") + .Matches(@"^(\+33|0)[6-7](\d{2}){4}$") + .WithMessage("Le numéro de téléphone portable n'est pas valide. Format attendu : +33XXXXXXXXX ou 0XXXXXXXXX.") + .MaximumLength(15).WithMessage("Le téléphone portable ne doit pas dépasser 15 caractères."); + + RuleFor(x => x.Email) + .NotEmpty().WithMessage("L'email est requis.") + .EmailAddress().WithMessage("L'email n'est pas valide.") + .MaximumLength(50).WithMessage("L'email ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.IdSite) + .NotEmpty().WithMessage("L'ID du site est requis."); + + RuleFor(x => x.IdService) + .NotEmpty().WithMessage("L'ID du service est requis."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Salarie/SalarieUpdateValidator.cs b/ldap-cesi/Validator/Salarie/SalarieUpdateValidator.cs new file mode 100644 index 0000000..d364149 --- /dev/null +++ b/ldap-cesi/Validator/Salarie/SalarieUpdateValidator.cs @@ -0,0 +1,43 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Service; + +namespace ldap_cesi.Validator.Salarie; + +public class SalarieUpdateValidator : AbstractValidator +{ + public SalarieUpdateValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("L'identifiant du salarié est requis."); + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.Prenom) + .NotEmpty().WithMessage("Le prénom est requis.") + .MaximumLength(50).WithMessage("Le prénom ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.TelephoneFixe) + .NotEmpty().WithMessage("Le téléphone fixe est requis.") + .Matches(@"^(\+33|0)[1-9](\d{2}){4}$") + .WithMessage("Le numéro de téléphone fixe n'est pas valide. Format attendu : +33XXXXXXXXX ou 0XXXXXXXXX.") + .MaximumLength(15).WithMessage("Le téléphone fixe ne doit pas dépasser 15 caractères."); + + RuleFor(x => x.TelephonePortable) + .NotEmpty().WithMessage("Le téléphone portable est requis.") + .Matches(@"^(\+33|0)[6-7](\d{2}){4}$") + .WithMessage("Le numéro de téléphone portable n'est pas valide. Format attendu : +33XXXXXXXXX ou 0XXXXXXXXX.") + .MaximumLength(15).WithMessage("Le téléphone portable ne doit pas dépasser 15 caractères."); + + RuleFor(x => x.Email) + .NotEmpty().WithMessage("L'email est requis.") + .EmailAddress().WithMessage("L'email n'est pas valide.") + .MaximumLength(50).WithMessage("L'email ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.IdSite) + .NotEmpty().WithMessage("L'ID du site est requis."); + + RuleFor(x => x.IdService) + .NotEmpty().WithMessage("L'ID du service est requis."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Service/ServiceCreateValidator.cs b/ldap-cesi/Validator/Service/ServiceCreateValidator.cs new file mode 100644 index 0000000..48f58dd --- /dev/null +++ b/ldap-cesi/Validator/Service/ServiceCreateValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Service; + +namespace ldap_cesi.Validator.Service; + +public class ServiceCreateValidator : AbstractValidator +{ + public ServiceCreateValidator() + { + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Service/ServiceUpdateValidator.cs b/ldap-cesi/Validator/Service/ServiceUpdateValidator.cs new file mode 100644 index 0000000..a2362f5 --- /dev/null +++ b/ldap-cesi/Validator/Service/ServiceUpdateValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Service; + +namespace ldap_cesi.Validator.Service; + +public class ServiceUpdateValidator : AbstractValidator +{ + public ServiceUpdateValidator() + { + RuleFor(x => x.Nom) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(50).WithMessage("Le nom ne doit pas dépasser 50 caractères."); + RuleFor(x => x.Id) + .NotEmpty().WithMessage("L'identifiant du service est requis."); + + } + +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Site/SiteCreateValidator.cs b/ldap-cesi/Validator/Site/SiteCreateValidator.cs new file mode 100644 index 0000000..16ea713 --- /dev/null +++ b/ldap-cesi/Validator/Site/SiteCreateValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Site; + +namespace ldap_cesi.Validator.Site; + +public class SiteCreateValidator : AbstractValidator +{ + public SiteCreateValidator() + { + RuleFor(x => x.Ville) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(150).WithMessage("Le nom de la ville ne doit pas dépasser 150 caractères."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Site/SiteUpdateValidator.cs b/ldap-cesi/Validator/Site/SiteUpdateValidator.cs new file mode 100644 index 0000000..1efd5a5 --- /dev/null +++ b/ldap-cesi/Validator/Site/SiteUpdateValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs.Site; + +namespace ldap_cesi.Validator.Site; + +public class SiteUpdateValidator : AbstractValidator +{ + public SiteUpdateValidator() + { + RuleFor(x => x.Ville) + .NotEmpty().WithMessage("Le nom est requis.") + .MaximumLength(150).WithMessage("Le nom de la ville ne doit pas dépasser 150 caractères."); + RuleFor(x => x.Id) + .NotEmpty().WithMessage("L'identifiant du site est requis."); + } +} \ No newline at end of file diff --git a/ldap-cesi/Validator/Utilisateur/UtilisateurLoginValidator.cs b/ldap-cesi/Validator/Utilisateur/UtilisateurLoginValidator.cs new file mode 100644 index 0000000..aa2e28f --- /dev/null +++ b/ldap-cesi/Validator/Utilisateur/UtilisateurLoginValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using ldap_cesi.DTOs.Inputs; + +namespace ldap_cesi.Validator.Utilisateur; + +public class UtilisateurLoginValidator : AbstractValidator +{ + public UtilisateurLoginValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("L'email est requis.") + .EmailAddress().WithMessage("L'email n'est pas valide.") + .MaximumLength(50).WithMessage("L'email ne doit pas dépasser 50 caractères."); + + RuleFor(x => x.MotDePasse) + .NotEmpty().WithMessage("Le mot de passe est requis.") + .MinimumLength(6).WithMessage("Le mot de passe doit contenir au moins 6 caractères.") + .MaximumLength(20).WithMessage("Le mot de passe ne doit pas dépasser 20 caractères."); + } +} \ No newline at end of file diff --git a/ldap-cesi/ldap-cesi.csproj b/ldap-cesi/ldap-cesi.csproj new file mode 100644 index 0000000..06958b9 --- /dev/null +++ b/ldap-cesi/ldap-cesi.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + enable + enable + ldap_cesi + CS0472,CS1591,CS1587 + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + +