Compare commits

...

11 Commits

Author SHA1 Message Date
carteronm ff98045932 Merge vers main 2026-06-11 10:49:06 +02:00
carteronm e1e8a834b2 push fichiers inutiles 2026-06-11 10:48:28 +02:00
gokhoal 61f3d2889c chiffrement 2026-06-11 10:33:02 +02:00
carteronm 66a7e633a9 Rôles ajoutés 2026-06-11 01:49:21 +02:00
carteronm fff484d4ba Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	Knots/bin/Debug/net8.0/Knots.dll
#	Knots/bin/Debug/net8.0/Knots.exe
#	Knots/bin/Debug/net8.0/Knots.pdb
#	Knots/obj/Debug/net8.0/Knots.AssemblyInfo.cs
#	Knots/obj/Debug/net8.0/Knots.AssemblyInfoInputs.cache
#	Knots/obj/Debug/net8.0/Knots.dll
#	Knots/obj/Debug/net8.0/Knots.pdb
#	Knots/obj/Debug/net8.0/apphost.exe
#	Knots/obj/Debug/net8.0/ref/Knots.dll
#	Knots/obj/Debug/net8.0/refint/Knots.dll
2026-06-11 01:32:33 +02:00
carteronm e69550048b Préparation des rôles 2026-06-11 01:32:20 +02:00
gokhoal a29fb27b0d commit blabla3 2026-06-11 01:30:14 +02:00
gokhoal c23cc4a03a LES MESSAGES MARCHENT 2026-06-11 01:29:27 +02:00
gokhoal 42187ba088 commit de fichiers info 2026-04-03 09:06:35 +02:00
oistig cc90904228 gitignore 2026-03-12 15:02:49 +01:00
oistig 47cf23d740 premier push de fichiers inutiles 2026-03-12 15:02:10 +01:00
36 changed files with 284 additions and 52 deletions
+2
View File
@@ -0,0 +1,2 @@
/.idea/.idea.Knots/.idea/.name
/Knots/obj/rider.project.model.nuget.info
+2
View File
@@ -6,4 +6,6 @@ public class GetDiscussionDto
public string Name { get; set; } = string.Empty;
public bool IsGroup { get; set; }
public int? MembersCount { get; set; }
public int? GroupId { get; set; }
}
+1 -1
View File
@@ -7,6 +7,6 @@ public class GetMessageDetailsDto
public DateTime Date { get; set; }
public Boolean Type { get; set; }
public int AuthorId { get; set; }
public int UserId { get; set; }
public string AuthorName { get; set; } = "";
}
@@ -3,10 +3,11 @@ using Knots.DTO.Discussion;
using Knots.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Knots.Services;
namespace Knots.Endpoints.Discussion;
public class CreateGroupDiscussionEndpoint(KnotsDbContext db) : Endpoint<CreateGroupDiscussionRequest, GetDiscussionDto>
public class CreateGroupDiscussionEndpoint(KnotsDbContext db, EncryptionService encryption) : Endpoint<CreateGroupDiscussionRequest, GetDiscussionDto>
{
public override void Configure()
{
@@ -44,6 +45,7 @@ public class CreateGroupDiscussionEndpoint(KnotsDbContext db) : Endpoint<CreateG
Models.Discussion discussion = new()
{
IsGroup = true,
Key = new Models.Key { EnKey = encryption.GenerateKey() },
UserDiscussions = targets
.Select(t => new UserDiscussion { UserId = t.Id })
.Append(new UserDiscussion { UserId = currentUserId })
@@ -3,10 +3,11 @@ using Knots.DTO.Discussion;
using Knots.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Knots.Services;
namespace Knots.Endpoints.Discussion;
public class CreatePrivateDiscussionEndpoint(KnotsDbContext db)
public class CreatePrivateDiscussionEndpoint(KnotsDbContext db, EncryptionService encryption)
: Endpoint<CreatePrivateDiscussionRequest, GetDiscussionDto>
{
public override void Configure()
@@ -57,6 +58,7 @@ public class CreatePrivateDiscussionEndpoint(KnotsDbContext db)
Models.Discussion discussion = new()
{
IsGroup = false,
Key = new Models.Key { EnKey = encryption.GenerateKey() },
UserDiscussions =
[
new UserDiscussion { UserId = currentUserId },
@@ -0,0 +1,35 @@
using System.Security.Claims;
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
namespace Knots.Endpoints.Discussion;
public class GetDiscussionMembersEndpoint(KnotsDbContext db) : EndpointWithoutRequest<List<string>>
{
public override void Configure()
{
Get("/discussions/{discussionId}/members");
}
public override async Task HandleAsync(CancellationToken ct)
{
int discussionId = Route<int>("discussionId");
Models.Discussion? discussion = await db.Discussions
.Include(d => d.UserDiscussions)
.ThenInclude(ud => ud.User)
.SingleOrDefaultAsync(d => d.Id == discussionId, ct);
if (discussion is null)
{
await SendNotFoundAsync(ct);
return;
}
List<string> members = discussion.UserDiscussions
.Select(ud => ud.User.Username!)
.ToList();
await SendOkAsync(members, ct);
}
}
@@ -0,0 +1,52 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
namespace Knots.Endpoints.Discussion;
public class GetDiscussionMembersWithRolesEndpoint(KnotsDbContext db) : EndpointWithoutRequest<List<MemberWithRoleDto>>
{
public override void Configure()
{
Get("/discussions/{discussionId}/members/roles");
}
public override async Task HandleAsync(CancellationToken ct)
{
int discussionId = Route<int>("discussionId");
Models.Discussion? discussion = await db.Discussions
.Include(d => d.Group)
.ThenInclude(g => g!.GroupUsers)
.ThenInclude(gu => gu.User)
.Include(d => d.Group)
.ThenInclude(g => g!.GroupUsers)
.ThenInclude(gu => gu.Role)
.SingleOrDefaultAsync(d => d.Id == discussionId, ct);
if (discussion?.Group is null)
{
await SendNotFoundAsync(ct);
return;
}
List<MemberWithRoleDto> members = discussion.Group.GroupUsers
.Select(gu => new MemberWithRoleDto
{
UserId = gu.UserId,
Username = gu.User.Username!,
RoleId = gu.RoleId,
RoleLibelle = gu.Role?.Libelle
})
.ToList();
await SendOkAsync(members, ct);
}
}
public class MemberWithRoleDto
{
public int UserId { get; set; }
public string Username { get; set; } = "";
public int? RoleId { get; set; }
public string? RoleLibelle { get; set; }
}
@@ -27,7 +27,8 @@ public class GetMyDiscussionEndpoint(KnotsDbContext db) : EndpointWithoutRequest
.Where(ud => ud.UserId != userId)
.Select(ud => ud.User.Username)
.FirstOrDefault() ?? "",
MembersCount = null
MembersCount = null,
GroupId = null
});
// Discussions de groupe : l'utilisateur est membre du groupe
@@ -38,7 +39,8 @@ public class GetMyDiscussionEndpoint(KnotsDbContext db) : EndpointWithoutRequest
Id = d.Id,
IsGroup = true,
Name = d.Group!.Name!,
MembersCount = d.Group.MembersAmount
MembersCount = d.Group.MembersAmount,
GroupId = d.Group.Id
});
List<GetDiscussionDto> discussions = await privees.Concat(groupes).ToListAsync(ct);
+18 -10
View File
@@ -2,11 +2,12 @@ using System.Security.Claims;
using FastEndpoints;
using Knots.DTO.Message;
using Knots.DTO.User;
using Knots.Services;
using Microsoft.EntityFrameworkCore;
namespace Knots.Endpoints.Message;
public class GetMessageEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint<GetDiscussionMessagesRequest, List<GetMessageDetailsDto>>
public class GetMessageEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper, EncryptionService encryption) : Endpoint<GetDiscussionMessagesRequest, List<GetMessageDetailsDto>>
{
public override void Configure()
{
@@ -31,19 +32,26 @@ public class GetMessageEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) :
return;
}
List<GetMessageDetailsDto> messages = await db.Messages
string? key = await db.Discussions
.Where(d => d.Id == req.DiscussionId)
.Select(d => d.Key!.EnKey)
.SingleAsync(ct);
var rows = await db.Messages
.Where(m => m.DiscussionId == req.DiscussionId)
.OrderBy(m => m.Date)
.Select(m => new GetMessageDetailsDto
{
Id = m.Id,
Contenu = m.Contenu!,
Date = m.Date,
AuthorId = m.UserId,
AuthorName = m.User.Username!
})
.Select(m => new { m.Id, m.Contenu, m.Date, m.UserId, AuthorName = m.User.Username! })
.ToListAsync(ct);
List<GetMessageDetailsDto> messages = rows.Select(m => new GetMessageDetailsDto
{
Id = m.Id,
Contenu = encryption.Decrypt(m.Contenu!, key!),
Date = m.Date,
UserId = m.UserId,
AuthorName = m.AuthorName
}).ToList();
await SendOkAsync(messages, ct);
}
}
@@ -0,0 +1,45 @@
using FastEndpoints;
using Knots.Models;
using Microsoft.EntityFrameworkCore;
namespace Knots.Endpoints.Role;
public class AssignRoleEndpoint(KnotsDbContext db) : Endpoint<AssignRoleRequest>
{
public override void Configure()
{
Post("/groups/{groupId}/members/{userId}/role");
}
public override async Task HandleAsync(AssignRoleRequest req, CancellationToken ct)
{
int groupId = Route<int>("groupId");
int userId = Route<int>("userId");
GroupUser? groupUser = await db.GroupUsers
.SingleOrDefaultAsync(gu => gu.GroupId == groupId && gu.UserId == userId, ct);
if (groupUser is null)
{
await SendNotFoundAsync(ct);
return;
}
bool roleExists = await db.Roles.AnyAsync(r => r.Id == req.RoleId, ct);
if (!roleExists)
{
await SendNotFoundAsync(ct);
return;
}
groupUser.RoleId = req.RoleId;
await db.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}
public class AssignRoleRequest
{
public int RoleId { get; set; }
}
+18 -8
View File
@@ -1,22 +1,32 @@
using FastEndpoints;
using Knots.DTO.Role;
using Knots.DTO.User;
using Knots.Models;
namespace Knots.Endpoints.Role;
public class CreateRoleEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint<CreateRoleDto>
public class CreateRoleEndpoint(KnotsDbContext db) : Endpoint<CreateRoleRequest, RoleDto>
{
public override void Configure()
{
Post("/roles");
AllowAnonymous();
}
public override async Task HandleAsync(CreateRoleDto req, CancellationToken ct)
public override async Task HandleAsync(CreateRoleRequest req, CancellationToken ct)
{
Models.Role? role = mapper.Map<Models.Role>(req);
Models.Role role = new() { Libelle = req.Libelle };
db.Roles.Add(role);
await db.SaveChangesAsync(ct);
await SendNoContentAsync(ct);
await SendOkAsync(new RoleDto { Id = role.Id, Libelle = role.Libelle! }, ct);
}
}
public class CreateRoleRequest
{
public string Libelle { get; set; } = "";
}
public class RoleDto
{
public int Id { get; set; }
public string Libelle { get; set; } = "";
}
+30 -15
View File
@@ -1,45 +1,60 @@
using Microsoft.AspNetCore.Authorization;
using Knots.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace Knots.Hubs;
[Authorize]
public class ChatHub(KnotsDbContext db, AutoMapper.IMapper mapper) : Hub
public class ChatHub(KnotsDbContext db, AutoMapper.IMapper mapper, EncryptionService encryption) : Hub
{
// Rejoindre une conversation (room)
public async Task JoinConversation(string conversationId)
public async Task JoinConversation(string discussionId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, conversationId);
await Groups.AddToGroupAsync(Context.ConnectionId, discussionId);
}
// Quitter une conversation
public async Task LeaveConversation(string conversationId)
public async Task LeaveConversation(string discussionId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, conversationId);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, discussionId);
}
// Envoyer un message à une conversation
public async Task SendMessage(string conversationId, string content)
public async Task SendMessage(string discussionId, string content)
{
int id = int.Parse(discussionId);
Models.Discussion discussion = await db.Discussions
.Include(d => d.Key)
.SingleAsync(d => d.Id == id);
var message = new Models.Message
{
// adapte aux noms réels de ton modèle (AuthorId, Contenu, Date...)
AuthorId = int.Parse(Context.UserIdentifier!),
Contenu = content,
Date = DateTime.UtcNow,
DiscussionId = int.Parse(conversationId)
Contenu = encryption.Encrypt(content, discussion.Key!.EnKey!), // chiffré en base
Date = DateTime.UtcNow,
Type = false,
UserId = int.Parse(Context.UserIdentifier!),
DiscussionId = id
};
db.Messages.Add(message);
await db.SaveChangesAsync();
await Clients.Group(conversationId).SendAsync("ReceiveMessage", message);
// diffusion en clair, avec les noms de champs attendus par le front
await Clients.Group(discussionId).SendAsync("ReceiveMessage", new
{
id = message.Id,
contenu = content,
date = message.Date,
userId = message.UserId
});
}
// Notifier que l'utilisateur est en train d'écrire
public async Task Typing(string conversationId)
public async Task Typing(string discussionId)
{
await Clients.OthersInGroup(conversationId)
await Clients.OthersInGroup(discussionId)
.SendAsync("UserTyping", Context.UserIdentifier);
}
}
+3
View File
@@ -11,6 +11,9 @@ public class Discussion
public int? GroupId { get; set; }
public Group? Group { get; set; }
public int? KeyId { get; set; }
public Key? Key { get; set; }
public List<Message> Messages { get; set; } = [];
public List<UserDiscussion> UserDiscussions { get; set; } = [];
}
-1
View File
@@ -6,5 +6,4 @@ public class Key
{
[Key] public int Id { get; set; }
[Required, MaxLength(50)] public string? EnKey { get; set; }
public List<Message> Messages { get; set; } = [];
}
-3
View File
@@ -9,8 +9,6 @@ public class Message
[Required] public DateTime Date { get; set; }
[Required] public Boolean Type { get; set; }
public int AuthorId { get; set; }
public int UserId { get; set; }
public User User { get; set; } = null!;
@@ -18,5 +16,4 @@ public class Message
public Discussion Discussion { get; set; } = null!;
public Group? Group { get; set; }
public Key? Key { get; set; }
}
+2
View File
@@ -73,6 +73,8 @@ builder.Services.AddSignalR();
builder.Services.AddAutoMapper(cfg => { }, typeof(Program).Assembly);
builder.Services.AddSingleton<EncryptionService>();
// On construit l'application en lui donnant vie
WebApplication app = builder.Build();
+56
View File
@@ -0,0 +1,56 @@
using System.Security.Cryptography;
using System.Text;
namespace Knots.Services;
public class EncryptionService
{
private const int NonceSize = 12; // AesGcm.NonceByteSizes.MaxSize
private const int TagSize = 16; // AesGcm.TagByteSizes.MaxSize
// Génère une clé AES-256 (32 octets) encodée en Base64
public string GenerateKey()
=> Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
// Chiffre → renvoie Base64(nonce + tag + ciphertext)
public string Encrypt(string plainText, string base64Key)
{
byte[] key = Convert.FromBase64String(base64Key);
byte[] plain = Encoding.UTF8.GetBytes(plainText);
byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize);
byte[] cipher = new byte[plain.Length];
byte[] tag = new byte[TagSize];
using AesGcm aes = new(key, TagSize);
aes.Encrypt(nonce, plain, cipher, tag);
byte[] result = new byte[NonceSize + TagSize + cipher.Length];
Buffer.BlockCopy(nonce, 0, result, 0, NonceSize);
Buffer.BlockCopy(tag, 0, result, NonceSize, TagSize);
Buffer.BlockCopy(cipher, 0, result, NonceSize + TagSize, cipher.Length);
return Convert.ToBase64String(result);
}
// Déchiffre Base64(nonce + tag + ciphertext)
public string Decrypt(string base64Cipher, string base64Key)
{
byte[] key = Convert.FromBase64String(base64Key);
byte[] data = Convert.FromBase64String(base64Cipher);
byte[] nonce = new byte[NonceSize];
byte[] tag = new byte[TagSize];
byte[] cipher = new byte[data.Length - NonceSize - TagSize];
Buffer.BlockCopy(data, 0, nonce, 0, NonceSize);
Buffer.BlockCopy(data, NonceSize, tag, 0, TagSize);
Buffer.BlockCopy(data, NonceSize + TagSize, cipher, 0, cipher.Length);
byte[] plain = new byte[cipher.Length];
using AesGcm aes = new(key, TagSize);
aes.Decrypt(nonce, cipher, tag, plain);
return Encoding.UTF8.GetString(plain);
}
}
@@ -11,7 +11,7 @@ public class CreateMessageDtoValidator : Validator<CreateMessageDto>
RuleFor(x => x.Contenu)
.NotEmpty()
.WithMessage("Le message ne peux pas être vide")
.MaximumLength(1000)
.MaximumLength(2000)
.WithMessage("Le message ne doit pas faire plus de 1000 caractères");
RuleFor(x => x.Date)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Knots")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f1cdc5de19a96d553f0eaf2fbe65392602c54f85")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+42187ba0882a01c8f97b16b49c74efd5fbce9b12")]
[assembly: System.Reflection.AssemblyProductAttribute("Knots")]
[assembly: System.Reflection.AssemblyTitleAttribute("Knots")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
0618e480c1221c208fb53c91259f81c85675af45b5772bccc9fee873e927c02b
882a1ca1aec90fc8d6295b45413d9067b1a028d9de136ea4e9be02775fdc15d7
@@ -1 +1 @@
1afb5c57f375954b5501d52f6ccbfcdd0c9a4a7d75abca85439c878c12264607
8c008e0540a310c319081e4a6b8a7ce2f5c9b271d5260b8b45b33f49b9ad5228
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Knots")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+42187ba0882a01c8f97b16b49c74efd5fbce9b12")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+cc9090422803e15ae5e01cdb4964a0c908f54640")]
[assembly: System.Reflection.AssemblyProductAttribute("Knots")]
[assembly: System.Reflection.AssemblyTitleAttribute("Knots")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
882a1ca1aec90fc8d6295b45413d9067b1a028d9de136ea4e9be02775fdc15d7
2ca7620f113862464478e6b6389da9524ea0be35ffdaed378316ed68319d330f
@@ -9,13 +9,13 @@ build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Knots
build_property.RootNamespace = Knots
build_property.ProjectDir = /home/carteronm@stsio.lan/RiderProjects/Knots/Knots/
build_property.ProjectDir = C:\Users\oistig\RiderProjects\Knots\Knots\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.RazorLangVersion = 9.0
build_property.SupportLocalizedComponentNames =
build_property.GenerateRazorMetadataSourceChecksumAttributes =
build_property.MSBuildProjectDirectory = /home/carteronm@stsio.lan/RiderProjects/Knots/Knots
build_property.MSBuildProjectDirectory = C:\Users\oistig\RiderProjects\Knots\Knots
build_property._RazorSourceGeneratorDebug =
build_property.EffectiveAnalysisLevelStyle = 9.0
build_property.EnableCodeStyleSeverity =
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
17811302030866425
17811677268623865
+1 -1
View File
@@ -1 +1 @@
17811302053941977
17811677302595867