15 Commits

Author SHA1 Message Date
sanchezvem 09ce53203c Merge branch 'develop' 2026-06-08 12:22:09 +01:00
sanchezvem d17f2fb23e Implemented rustfs in app 2026-06-05 11:52:49 +01:00
sanchezvem 3065bba7fb Merge branch 'develop' 2026-06-01 11:13:22 +01:00
sanchezvem 697e1431d9 Added validators to managed users 2026-06-01 10:55:32 +01:00
sanchezvem de6a1c5385 Actualiser README.md 2026-05-30 14:25:02 +02:00
sanchezvem 29759cf896 Fixed wwwroot error 2026-05-30 13:44:21 +01:00
sanchezvem da1407579d Actualiser README.md 2026-05-30 14:23:44 +02:00
sanchezvem 602b91006e Merge remote-tracking branch 'origin/develop' 2026-05-30 13:45:04 +01:00
sanchezvem f0ad9b536a Changed values of userDto 2026-05-28 15:50:57 +01:00
sanchezvem fb97729c71 Changed values of userDto 2026-05-28 15:41:07 +01:00
sanchezvem 1bd92a8732 Fix route error 2026-05-28 15:39:55 +01:00
sanchezvem 8c38255ed9 Merge remote-tracking branch 'origin/feature/refactor-backend' into feature/refactor-backend
# Conflicts:
#	PyroFetes/Endpoints/Users/GetUserEndpoint.cs
2026-05-28 15:39:04 +01:00
sanchezvem 639631a63b Changed Id value 2026-05-28 15:36:58 +01:00
Cristiano 7f3ffde3ff Merge branch 'develop' 2025-11-27 14:56:14 +01:00
sanchezvem 6be43958fa updated length of password to 60 for bcrypt length 2025-11-27 13:28:05 +01:00
16 changed files with 241 additions and 67 deletions
@@ -5,7 +5,6 @@ public class GetUserDto
public int Id { get; set; } public int Id { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Password { get; set; } public string? Password { get; set; }
public string? Salt { get; set; }
public string? Fonction { get; set; } public string? Fonction { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
} }
@@ -12,7 +12,7 @@ public class UpdateDelivererEndpoint(DeliverersRepository deliverersRepository,
public override void Configure() public override void Configure()
{ {
Put("/deliverers/{@Id}", x => new { x.Id }); Put("/deliverers/{@Id}", x => new { x.Id });
Roles("Admin"); Roles("Admin", "Employe");
} }
public override async Task HandleAsync(UpdateDelivererDto req, CancellationToken ct) public override async Task HandleAsync(UpdateDelivererDto req, CancellationToken ct)
@@ -16,7 +16,7 @@ public class DeleteProductFromQuotationEndpoint(QuotationProductsRepository quot
public override void Configure() public override void Configure()
{ {
Delete("/quotations/{@ProductId}/{@QuotationId}", x => new { x.ProductId, x.QuotationId }); Delete("/quotations/{@ProductId}/{@QuotationId}", x => new { x.ProductId, x.QuotationId });
Roles("Admin"); Roles("Admin", "Employe");
} }
public override async Task HandleAsync(DeleteQuotationProductRequest req, CancellationToken ct) public override async Task HandleAsync(DeleteQuotationProductRequest req, CancellationToken ct)
@@ -2,10 +2,11 @@
using PyroFetes.DTO.SettingDTO.Response; using PyroFetes.DTO.SettingDTO.Response;
using PyroFetes.Models; using PyroFetes.Models;
using PyroFetes.Repositories; using PyroFetes.Repositories;
using PyroFetes.Services;
namespace PyroFetes.Endpoints.Settings; namespace PyroFetes.Endpoints.Settings;
public class GetSettingEndpoint(SettingsRepository settingsRepository, AutoMapper.IMapper mapper) : EndpointWithoutRequest<GetSettingDto> public class GetSettingEndpoint(SettingsRepository settingsRepository, StorageService storageService) : EndpointWithoutRequest<GetSettingDto>
{ {
public override void Configure() public override void Configure()
{ {
@@ -22,7 +23,14 @@ public class GetSettingEndpoint(SettingsRepository settingsRepository, AutoMappe
await Send.NotFoundAsync(ct); await Send.NotFoundAsync(ct);
return; return;
} }
GetSettingDto settingDto = new()
{
Id = setting.Id,
ElectronicSignature = storageService.GetUrl(setting.ElectronicSignature!),
Logo = storageService.GetUrl(setting.Logo!)
};
await Send.OkAsync(mapper.Map<GetSettingDto>(setting), ct); await Send.OkAsync(settingDto, ct);
} }
} }
@@ -2,10 +2,11 @@
using PyroFetes.DTO.SettingDTO.Request; using PyroFetes.DTO.SettingDTO.Request;
using PyroFetes.Models; using PyroFetes.Models;
using PyroFetes.Repositories; using PyroFetes.Repositories;
using PyroFetes.Services;
namespace PyroFetes.Endpoints.Settings; namespace PyroFetes.Endpoints.Settings;
public class PatchSettingElectronicSignatureEndpoint(SettingsRepository settingsRepository) : Endpoint<PatchSettingElectronicSignatureDto> public class PatchSettingElectronicSignatureEndpoint(SettingsRepository settingsRepository, StorageService storageService) : Endpoint<PatchSettingElectronicSignatureDto>
{ {
public override void Configure() public override void Configure()
{ {
@@ -24,14 +25,10 @@ public class PatchSettingElectronicSignatureEndpoint(SettingsRepository settings
await Send.NotFoundAsync(ct); await Send.NotFoundAsync(ct);
return; return;
} }
// Encodage en base64 string key = await storageService.UploadFile(req.ElectronicSignature!, "electronicSignature", ct);
using MemoryStream memoryStream = new(); setting.ElectronicSignature = key;
if (req.ElectronicSignature != null) await req.ElectronicSignature.CopyToAsync(memoryStream, ct);
byte[] signatureBytes = memoryStream.ToArray();
setting.ElectronicSignature = Convert.ToBase64String(signatureBytes);
await settingsRepository.SaveChangesAsync(ct); await settingsRepository.SaveChangesAsync(ct);
await Send.NoContentAsync(ct); await Send.NoContentAsync(ct);
} }
@@ -2,10 +2,11 @@
using PyroFetes.DTO.SettingDTO.Request; using PyroFetes.DTO.SettingDTO.Request;
using PyroFetes.Models; using PyroFetes.Models;
using PyroFetes.Repositories; using PyroFetes.Repositories;
using PyroFetes.Services;
namespace PyroFetes.Endpoints.Settings; namespace PyroFetes.Endpoints.Settings;
public class PatchSettingLogoEndpoint(SettingsRepository settingsRepository) : Endpoint<PatchSettingLogoDto> public class PatchSettingLogoEndpoint(SettingsRepository settingsRepository, StorageService storageService) : Endpoint<PatchSettingLogoDto>
{ {
public override void Configure() public override void Configure()
{ {
@@ -24,12 +25,9 @@ public class PatchSettingLogoEndpoint(SettingsRepository settingsRepository) : E
return; return;
} }
// Encodage en base64 string key = await storageService.UploadFile(req.Logo!, "logo", ct);
using MemoryStream memoryStream = new();
if (req.Logo != null) await req.Logo.CopyToAsync(memoryStream, ct);
byte[] logoBytes = memoryStream.ToArray();
setting.Logo = Convert.ToBase64String(logoBytes); setting.Logo = key;
await settingsRepository.SaveChangesAsync(ct); await settingsRepository.SaveChangesAsync(ct);
await Send.NoContentAsync(ct); await Send.NoContentAsync(ct);
+5 -9
View File
@@ -6,22 +6,18 @@ using PyroFetes.Specifications.Users;
namespace PyroFetes.Endpoints.Users; namespace PyroFetes.Endpoints.Users;
public class GetUserRequest public class GetUserEndpoint(UsersRepository usersRepository, AutoMapper.IMapper mapper) : EndpointWithoutRequest<GetUserDto>
{
public int Id { get; set; }
}
public class GetUserEndpoint(UsersRepository usersRepository, AutoMapper.IMapper mapper) : Endpoint<GetUserRequest, GetUserDto>
{ {
public override void Configure() public override void Configure()
{ {
Get("/users/{@Id}", x => new { x.Id }); Get("/user/");
Roles("Admin","Employe"); Roles("Admin","Employe");
} }
public override async Task HandleAsync(GetUserRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
User? user = await usersRepository.SingleOrDefaultAsync(new GetUserByIdSpec(req.Id), ct); int userId = int.Parse(User.FindFirst("Id")!.Value);
User? user = await usersRepository.SingleOrDefaultAsync(new GetUserByIdSpec(userId), ct);
if (user is null) if (user is null)
{ {
+29
View File
@@ -1,3 +1,5 @@
using Amazon.S3;
using Amazon.S3.Model;
using AutoMapper; using AutoMapper;
using AutoMapper.EquivalencyExpression; using AutoMapper.EquivalencyExpression;
using PyroFetes; using PyroFetes;
@@ -7,6 +9,7 @@ using FastEndpoints.Security;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using PyroFetes.MappingProfiles; using PyroFetes.MappingProfiles;
using PyroFetes.Repositories; using PyroFetes.Repositories;
using PyroFetes.Services;
using PyroFetes.Services.Pdf; using PyroFetes.Services.Pdf;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -56,6 +59,7 @@ builder.Services.AddScoped<CustomersRepository>();
builder.Services.AddScoped<IDeliveryNotePdfService, DeliveryNotePdfService>(); builder.Services.AddScoped<IDeliveryNotePdfService, DeliveryNotePdfService>();
builder.Services.AddScoped<IPurchaseOrderPdfService, PurchaseOrderPdfService>(); builder.Services.AddScoped<IPurchaseOrderPdfService, PurchaseOrderPdfService>();
builder.Services.AddScoped<IQuotationPdfService, QuotationPdfService>(); builder.Services.AddScoped<IQuotationPdfService, QuotationPdfService>();
builder.Services.AddScoped<StorageService>();
MapperConfiguration mappingConfig = new(mc => MapperConfiguration mappingConfig = new(mc =>
{ {
@@ -68,6 +72,31 @@ MapperConfiguration mappingConfig = new(mc =>
AutoMapper.IMapper mapper = mappingConfig.CreateMapper(); AutoMapper.IMapper mapper = mappingConfig.CreateMapper();
builder.Services.AddSingleton(mapper); builder.Services.AddSingleton(mapper);
// RUSTFS
IConfigurationSection config = builder.Configuration.GetSection("RustFS");
AmazonS3Client s3Client = new(
config["AccessKey"],
config["SecretKey"],
new AmazonS3Config
{
ServiceURL = config["ServiceUrl"],
ForcePathStyle = true
}
);
ListBucketsResponse? buckets = await s3Client.ListBucketsAsync();
bool exist = buckets?.Buckets?.Any(x => x.BucketName == config["BucketName"]) == true;
if (!exist)
{
await s3Client.PutBucketAsync(new PutBucketRequest
{
BucketName = config["BucketName"]
});
}
builder.Services.AddSingleton<IAmazonS3>(s3Client);
// On construit l'application en lui donnant vie // On construit l'application en lui donnant vie
WebApplication app = builder.Build(); WebApplication app = builder.Build();
app.UseAuthentication() app.UseAuthentication()
+1 -1
View File
@@ -10,6 +10,7 @@
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="9.3.1" /> <PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="9.3.1" />
<PackageReference Include="AutoMapper" Version="15.0.1" /> <PackageReference Include="AutoMapper" Version="15.0.1" />
<PackageReference Include="AutoMapper.Collection" Version="11.0.0" /> <PackageReference Include="AutoMapper.Collection" Version="11.0.0" />
<PackageReference Include="AWSSDK.S3" Version="4.0.24" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FastEndpoints" Version="7.0.1" /> <PackageReference Include="FastEndpoints" Version="7.0.1" />
<PackageReference Include="FastEndpoints.Security" Version="7.0.1" /> <PackageReference Include="FastEndpoints.Security" Version="7.0.1" />
@@ -33,7 +34,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Migrations\" /> <Folder Include="Migrations\" />
<Folder Include="wwwroot\" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+37
View File
@@ -0,0 +1,37 @@
using Amazon.S3;
using Amazon.S3.Model;
namespace PyroFetes.Services;
public class StorageService(IAmazonS3 amazonS3, IConfiguration config)
{
private readonly string _bucket = config["RustFs:BucketName"]!;
private readonly string _url = config["RustFs:ServiceUrl"]!;
public async Task<string> UploadFile(IFormFile file, string type, CancellationToken ct)
{
if (file.Length == 0) throw new Exception("Fichier vide");
string key = $"settings/{type}";
using MemoryStream memoryStream = new();
await file.CopyToAsync(memoryStream, ct);
memoryStream.Position = 0;
PutObjectRequest uploadRequest = new()
{
BucketName = _bucket,
ContentType = file.ContentType,
InputStream = memoryStream,
Key = key,
};
await amazonS3.PutObjectAsync(uploadRequest, ct);
return key;
}
public string? GetUrl(string key)
{
return string.IsNullOrEmpty(key) ? null : $"{_url}/{_bucket}/{key}";
}
}
@@ -0,0 +1,23 @@
using FastEndpoints;
using FluentValidation;
using PyroFetes.DTO.User.Request;
namespace PyroFetes.Validators.Users;
public class ConnectUserDtoValidator : Validator<ConnectUserDto>
{
public ConnectUserDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Username is required")
.MaximumLength(50)
.WithMessage("Username cannot exceed 50 characters")
.MinimumLength(2)
.WithMessage("Username must exceed 2 characters");
RuleFor(x => x.Password)
.NotEmpty()
.WithMessage("Password is required");
}
}
@@ -0,0 +1,26 @@
using FastEndpoints;
using FluentValidation;
using PyroFetes.DTO.User.Request;
namespace PyroFetes.Validators.Users;
public class CreateUpdateUserDtoValidator: Validator<UpdateUserDto>
{
public CreateUpdateUserDtoValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("L'email est requis")
.MaximumLength(100)
.WithMessage("L'email ne doit pas dépasser plus de 100 caractères")
.EmailAddress()
.WithMessage("Adresse email invalide");
RuleFor(x => x.Password)
.NotEmpty()
.WithMessage("Le mot de passe est requis")
.MinimumLength(12)
.WithMessage("Le mot de passe doit contenir au minimum 12 caractères")
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*?[#?_!@$%^&*-])");
}
}
@@ -0,0 +1,26 @@
using FastEndpoints;
using FluentValidation;
using PyroFetes.DTO.User.Request;
namespace PyroFetes.Validators.Users;
public class CreateUserDtoValidator : Validator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("L'email est requis")
.MaximumLength(100)
.WithMessage("L'email ne doit pas dépasser plus de 100 caractères")
.EmailAddress()
.WithMessage("Adresse email invalide");
RuleFor(x => x.Password)
.NotEmpty()
.WithMessage("Le mot de passe est requis")
.MinimumLength(12)
.WithMessage("Le mot de passe doit contenir au minimum 12 caractères")
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*?[#?_!@$%^&*-])");
}
}
+6
View File
@@ -4,5 +4,11 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"RustFS": {
"ServiceUrl": "https://stockage.sanchezvende.fr",
"AccessKey": "Admin_Beready_Exam2026",
"SecretKey": "4dm1n-Pr0j_2026-B3r34d7",
"BucketName": "images"
} }
} }
View File
+66 -37
View File
@@ -1,50 +1,79 @@
# Gestionnaire de Stocks et Commandes # PyroFêtes — Système de gestion des stocks et des documents
Cette application web permet de **suivre les stocks**, **automatiser les commandes fournisseurs** et **gérer le cycle complet dapprovisionnement**. > Application web de gestion des stocks, fournisseurs, devis, bons de commande et bons de livraison pour l'entreprise **PyroFêtes**.
Elle est conçue pour simplifier le travail des entreprises en offrant une vue en temps réel sur les produits, leurs fournisseurs et l’état des livraisons.
---
## ✨ Fonctionnalités principales ## Sommaire
### 1️⃣ Suivi et réapprovisionnement des stocks - [Contexte](#contexte)
- Définissez un **niveau minimal de stock** pour chaque produit. - [Fonctionnalités](#fonctionnalités)
- Surveillez les **niveaux en temps réel** grâce à une interface claire. - [Stack technique](#stack-technique)
- Lorsquun produit atteint ou descend sous son seuil minimal, le système **génère automatiquement un bon de commande** pour le réapprovisionner. - [Équipe](#équipe)
### 2️⃣ Gestion des fournisseurs
- Enregistrez les informations complètes des fournisseurs : nom, adresse, coordonnées, produits fournis, délais de livraison.
- **Associez un ou plusieurs fournisseurs** à chaque produit.
- Lorsquun bon de commande est créé, le système **propose automatiquement les fournisseurs appropriés**.
### 3️⃣ Devis et bons de commande ## Contexte
- Créez des **devis personnalisés** : sélection des produits, quantités, prix, ajout dun logo, message ou conditions de vente.
- **Imprimez ou exportez** vos devis au format PDF.
- Générez des **bons de commande** en quelques clics, avec personnalisation (logo, conditions dachat) et exportation en PDF.
### 4️⃣ Suivi des livraisons PyroFêtes cherchait à remplacer ses processus manuels de gestion des stocks et de génération de documents commerciaux par un outil centralisé. Les objectifs principaux sont :
- **Transformez un bon de commande en bon de livraison** dès lexpédition des produits par le fournisseur.
- Enregistrez toutes les informations importantes : date dexpédition, transporteur, numéro de suivi, date prévue et date effective de livraison.
- Recevez des **alertes en cas de retard**.
- Gérez la **réception des produits** et vérifiez leur conformité.
--- - Automatiser le réapprovisionnement
- Gérer les fournisseurs et leurs conditions
- Éditer et exporter les documents commerciaux (devis, bons de commande, bons de livraison)
- Assurer un suivi fiable des livraisons et des réceptions
## 🗂️ Livrables prévus
- **Modèle de données** : diagramme de classes commun à tous les groupes.
- **Interface utilisateur** : maquettes ou prototypes interactifs.
- **Code source commenté** pour une meilleure compréhension.
- **Documentation technique** : description des fonctionnalités, architecture de lapplication et API.
--- ## Fonctionnalités
## 👥 Équipe ### Gestion des stocks
- **Mathys** - Définition de seuils minimaux par produit
- **Enzo** - Visualisation en temps réel du stock courant
- **Cristiano** - Alertes automatiques en cas de stock sous le seuil
- **Arsène** - Génération automatique de bons de commande
--- ### Gestion des fournisseurs
- Enregistrement des fournisseurs (nom, adresse, coordonnées, conditions)
- Gestion des délais de livraison par produit
- Association de plusieurs fournisseurs à un produit (prix + délai)
- Suggestion automatique du fournisseur le plus pertinent
## 🚀 Objectif ### Devis & Bons de commande
Fournir un outil complet pour automatiser la gestion des stocks et des commandes, réduisant les erreurs humaines, améliorant le suivi des livraisons et facilitant la communication avec les fournisseurs. - Création de devis et bons de commande (produits, quantités, prix, remises)
- Personnalisation des documents (logo, message, conditions)
- Export au format **PDF**
### Bons de livraison & Réceptions
- Transformation d'un bon de commande validé en bon de livraison
- Enregistrement des informations de livraison (transporteur, numéro de suivi, dates)
- Alertes en cas de retard de livraison
- Gestion des réceptions avec mise à jour automatique des stocks
## Stack technique
| Couche | Technologie |
|---|---|
| **Front-end** | Angular + NG-ZORRO + Tailwind CSS |
| **Back-end** | C# / .NET |
| **API** | REST (C#) |
| **Base de données** | SQL Server |
| **Gestion des tâches** | YouTrack |
| **Versioning** | Gitea |
| **Communication** | Discord + Présentiel |
## Équipe
| Membre | Rôle |
|---|---|
| Mathys Sanchez-Vendé | Développeur |
| Enzo Norguet | Développeur |
| Cristiano Henrique Gaspar | Développeur |
| Arsène | Développeur |
**Clients :** Mr Thibault Ferrand, Mr Douguet
## Sécurité
- Authentification sécurisée
- Gestion des accès et des permissions par rôle (Commercial / Administrateur)
- Protection des données utilisateurs