diff --git a/.idea/.idea.Knots/.idea/dataSources.xml b/.idea/.idea.Knots/.idea/dataSources.xml
new file mode 100644
index 0000000..3b05a4b
--- /dev/null
+++ b/.idea/.idea.Knots/.idea/dataSources.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ sqlserver.jb
+ true
+ com.jetbrains.jdbc.sqlserver.SqlServerDriver
+ Server=romaric-thibault.fr,1433
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/Knots/DTO/Discussion/CreateDiscussionDto.cs b/Knots/DTO/Discussion/CreateDiscussionDto.cs
new file mode 100644
index 0000000..1e9e9d6
--- /dev/null
+++ b/Knots/DTO/Discussion/CreateDiscussionDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Discussion;
+
+public class CreateDiscussionDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Discussion/DeleteDiscussionDto.cs b/Knots/DTO/Discussion/DeleteDiscussionDto.cs
new file mode 100644
index 0000000..8b543dd
--- /dev/null
+++ b/Knots/DTO/Discussion/DeleteDiscussionDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Discussion;
+
+public class DeleteDiscussionDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Discussion/GetDiscussionDto.cs b/Knots/DTO/Discussion/GetDiscussionDto.cs
new file mode 100644
index 0000000..9b9086c
--- /dev/null
+++ b/Knots/DTO/Discussion/GetDiscussionDto.cs
@@ -0,0 +1,11 @@
+namespace Knots.DTO.Discussion;
+
+public class GetDiscussionDto
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public bool IsGroup { get; set; }
+ public int? MembersCount { get; set; }
+
+ public int? GroupId { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/CreateGroupDto.cs b/Knots/DTO/Group/CreateGroupDto.cs
new file mode 100644
index 0000000..327d750
--- /dev/null
+++ b/Knots/DTO/Group/CreateGroupDto.cs
@@ -0,0 +1,8 @@
+namespace Knots.DTO.Group;
+
+public class CreateGroupDto
+{
+ public string? Nom { get; set; }
+ public int NombreMembres { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/DeleteGroupDto.cs b/Knots/DTO/Group/DeleteGroupDto.cs
new file mode 100644
index 0000000..e599b0c
--- /dev/null
+++ b/Knots/DTO/Group/DeleteGroupDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Group;
+
+public class DeleteGroupDto
+{
+ public int? Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/GetGroupDetailsDto.cs b/Knots/DTO/Group/GetGroupDetailsDto.cs
new file mode 100644
index 0000000..ebb220c
--- /dev/null
+++ b/Knots/DTO/Group/GetGroupDetailsDto.cs
@@ -0,0 +1,9 @@
+namespace Knots.DTO.Group;
+
+public class GetGroupDetailsDto
+{
+ public int Id { get; set; }
+ public string? Nom { get; set; }
+ public int NombreMembres { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/GetGroupDto.cs b/Knots/DTO/Group/GetGroupDto.cs
new file mode 100644
index 0000000..8338582
--- /dev/null
+++ b/Knots/DTO/Group/GetGroupDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Group;
+
+public class GetGroupDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/UpdateGroupDto.cs b/Knots/DTO/Group/UpdateGroupDto.cs
new file mode 100644
index 0000000..bf01672
--- /dev/null
+++ b/Knots/DTO/Group/UpdateGroupDto.cs
@@ -0,0 +1,9 @@
+namespace Knots.DTO.Group;
+
+public class UpdateGroupDto
+{
+ public int Id { get; set; }
+ public string? Nom { get; set; }
+ public int NombreMembres { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/UpdateGroupMembersAmountDto.cs b/Knots/DTO/Group/UpdateGroupMembersAmountDto.cs
new file mode 100644
index 0000000..2f460eb
--- /dev/null
+++ b/Knots/DTO/Group/UpdateGroupMembersAmountDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.Group;
+
+public class UpdateGroupMembersAmountDto
+{
+ public int Id { get; set; }
+ public int MembersAmount { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/UpdateGroupNameDto.cs b/Knots/DTO/Group/UpdateGroupNameDto.cs
new file mode 100644
index 0000000..4f212a9
--- /dev/null
+++ b/Knots/DTO/Group/UpdateGroupNameDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.Group;
+
+public class UpdateGroupNameDto
+{
+ public int Id { get; set; }
+ public string? Name { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Group/UpdateGroupProfilePictureDto.cs b/Knots/DTO/Group/UpdateGroupProfilePictureDto.cs
new file mode 100644
index 0000000..022229a
--- /dev/null
+++ b/Knots/DTO/Group/UpdateGroupProfilePictureDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.Group;
+
+public class UpdateGroupProfilePictureDto
+{
+ public int Id { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Key/CreateKeyDto.cs b/Knots/DTO/Key/CreateKeyDto.cs
new file mode 100644
index 0000000..0282cbb
--- /dev/null
+++ b/Knots/DTO/Key/CreateKeyDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Key;
+
+public class CreateKeyDto
+{
+ public string? EnKey { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Key/DeleteKeyDto.cs b/Knots/DTO/Key/DeleteKeyDto.cs
new file mode 100644
index 0000000..a08c83d
--- /dev/null
+++ b/Knots/DTO/Key/DeleteKeyDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Key;
+
+public class DeleteKeyDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Key/GetKeyDetailsDto.cs b/Knots/DTO/Key/GetKeyDetailsDto.cs
new file mode 100644
index 0000000..56e2dd5
--- /dev/null
+++ b/Knots/DTO/Key/GetKeyDetailsDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.Key;
+
+public class GetKeyDetailsDto
+{
+ public int Id { get; set; }
+ public string? EnKey { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Key/GetKeyDto.cs b/Knots/DTO/Key/GetKeyDto.cs
new file mode 100644
index 0000000..0a5c7c4
--- /dev/null
+++ b/Knots/DTO/Key/GetKeyDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Key;
+
+public class GetKeyDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Message/CreateMessageDto.cs b/Knots/DTO/Message/CreateMessageDto.cs
new file mode 100644
index 0000000..c07e671
--- /dev/null
+++ b/Knots/DTO/Message/CreateMessageDto.cs
@@ -0,0 +1,8 @@
+namespace Knots.DTO.Message;
+
+public class CreateMessageDto
+{
+ public string? Contenu { get; set; }
+ public DateTime Date { get; set; }
+ public Boolean Type { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Message/DeleteMessageDto.cs b/Knots/DTO/Message/DeleteMessageDto.cs
new file mode 100644
index 0000000..4433379
--- /dev/null
+++ b/Knots/DTO/Message/DeleteMessageDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Message;
+
+public class DeleteMessageDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Message/GetMessageDetailsDto.cs b/Knots/DTO/Message/GetMessageDetailsDto.cs
new file mode 100644
index 0000000..dded1d6
--- /dev/null
+++ b/Knots/DTO/Message/GetMessageDetailsDto.cs
@@ -0,0 +1,12 @@
+namespace Knots.DTO.Message;
+
+public class GetMessageDetailsDto
+{
+ public int Id { get; set; }
+ public string? Contenu { get; set; }
+ public DateTime Date { get; set; }
+ public Boolean Type { get; set; }
+
+ public int UserId { get; set; }
+ public string AuthorName { get; set; } = "";
+}
\ No newline at end of file
diff --git a/Knots/DTO/Message/GetMessageDto.cs b/Knots/DTO/Message/GetMessageDto.cs
new file mode 100644
index 0000000..a3e4f62
--- /dev/null
+++ b/Knots/DTO/Message/GetMessageDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Message;
+
+public class GetMessageDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Role/CreateRoleDto.cs b/Knots/DTO/Role/CreateRoleDto.cs
new file mode 100644
index 0000000..e09b100
--- /dev/null
+++ b/Knots/DTO/Role/CreateRoleDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Role;
+
+public class CreateRoleDto
+{
+ public string? Libelle { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Role/DeleteRoleDto.cs b/Knots/DTO/Role/DeleteRoleDto.cs
new file mode 100644
index 0000000..eee1a5e
--- /dev/null
+++ b/Knots/DTO/Role/DeleteRoleDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Role;
+
+public class DeleteRoleDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/Role/GetRoleDto.cs b/Knots/DTO/Role/GetRoleDto.cs
new file mode 100644
index 0000000..ef944a7
--- /dev/null
+++ b/Knots/DTO/Role/GetRoleDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.Role;
+
+public class GetRoleDto
+{
+ public string? Libelle { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/CreateUserDto.cs b/Knots/DTO/User/CreateUserDto.cs
new file mode 100644
index 0000000..5764ef9
--- /dev/null
+++ b/Knots/DTO/User/CreateUserDto.cs
@@ -0,0 +1,11 @@
+namespace Knots.DTO.User;
+
+public class CreateUserDto
+{
+ public string? Username { get; set; }
+ public string? Description {get; set;}
+ public string? Password { get; set; }
+ public string? Email { get; set; }
+ public string? Tel { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/DeleteUserDto.cs b/Knots/DTO/User/DeleteUserDto.cs
new file mode 100644
index 0000000..3768952
--- /dev/null
+++ b/Knots/DTO/User/DeleteUserDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.User;
+
+public class DeleteUserDto
+{
+ public string? Username { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/GetUserDetailsDto.cs b/Knots/DTO/User/GetUserDetailsDto.cs
new file mode 100644
index 0000000..98614c7
--- /dev/null
+++ b/Knots/DTO/User/GetUserDetailsDto.cs
@@ -0,0 +1,11 @@
+namespace Knots.DTO.User;
+
+public class GetUserDetailsDto
+{
+ public string? Username { get; set; }
+ public string? Description {get; set;}
+ public string? Password { get; set; }
+ public string? Email { get; set; }
+ public string? Tel { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/GetUserDto.cs b/Knots/DTO/User/GetUserDto.cs
new file mode 100644
index 0000000..e2bb8ad
--- /dev/null
+++ b/Knots/DTO/User/GetUserDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.User;
+
+public class GetUserDto
+{
+ public string? Username { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/LoginResponseDto.cs b/Knots/DTO/User/LoginResponseDto.cs
new file mode 100644
index 0000000..c05156e
--- /dev/null
+++ b/Knots/DTO/User/LoginResponseDto.cs
@@ -0,0 +1,12 @@
+namespace Knots.DTO.User;
+
+public class LoginResponseDto
+{
+ public string? Token { get; set; }
+ public int Id { get; set; }
+ public string? Username { get; set; }
+ public string? Email { get; set; }
+ public string? Tel { get; set; }
+ public string? ProfilePicture { get; set; }
+ public string? Description { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/LoginUserDto.cs b/Knots/DTO/User/LoginUserDto.cs
new file mode 100644
index 0000000..21d2e63
--- /dev/null
+++ b/Knots/DTO/User/LoginUserDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.User;
+
+public class LoginUserDto
+{
+ public string? Username { get; set; }
+ public string? Password { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUserContactDto.cs b/Knots/DTO/User/UpdateUserContactDto.cs
new file mode 100644
index 0000000..94363f9
--- /dev/null
+++ b/Knots/DTO/User/UpdateUserContactDto.cs
@@ -0,0 +1,8 @@
+namespace Knots.DTO.User;
+
+public class UpdateUserContactDto
+{
+ public int Id { get; set; }
+ public string? Email { get; set; }
+ public string? Tel { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUserDescriptionDto.cs b/Knots/DTO/User/UpdateUserDescriptionDto.cs
new file mode 100644
index 0000000..1d23b37
--- /dev/null
+++ b/Knots/DTO/User/UpdateUserDescriptionDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.User;
+
+public class UpdateUserDescriptionDto
+{
+ public int Id { get; set; }
+ public string? Description {get; set;}
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUserDto.cs b/Knots/DTO/User/UpdateUserDto.cs
new file mode 100644
index 0000000..d4397ca
--- /dev/null
+++ b/Knots/DTO/User/UpdateUserDto.cs
@@ -0,0 +1,6 @@
+namespace Knots.DTO.User;
+
+public class UpdateUserDto
+{
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUserPasswordDto.cs b/Knots/DTO/User/UpdateUserPasswordDto.cs
new file mode 100644
index 0000000..db83ddb
--- /dev/null
+++ b/Knots/DTO/User/UpdateUserPasswordDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.User;
+
+public class UpdateUserPasswordDto
+{
+ public int Id { get; set; }
+ public string? Password { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUserProfilePictureDto.cs b/Knots/DTO/User/UpdateUserProfilePictureDto.cs
new file mode 100644
index 0000000..9478cab
--- /dev/null
+++ b/Knots/DTO/User/UpdateUserProfilePictureDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.User;
+
+public class UpdateUserProfilePictureDto
+{
+ public int Id { get; set; }
+ public string? ProfilePicture { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/DTO/User/UpdateUsernameDto.cs b/Knots/DTO/User/UpdateUsernameDto.cs
new file mode 100644
index 0000000..44d09e7
--- /dev/null
+++ b/Knots/DTO/User/UpdateUsernameDto.cs
@@ -0,0 +1,7 @@
+namespace Knots.DTO.User;
+
+public class UpdateUsernameDto
+{
+ public int Id { get; set; }
+ public string? Username { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/CreateDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/CreateDiscussionEndpoint.cs
new file mode 100644
index 0000000..29579ce
--- /dev/null
+++ b/Knots/Endpoints/Discussion/CreateDiscussionEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Discussion;
+
+namespace Knots.Endpoints.Discussion;
+
+public class CreateDiscussionEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/discussions");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateDiscussionDto req, CancellationToken ct)
+ {
+ Models.Discussion? discussion = mapper.Map(req);
+ db.Discussions.Add(discussion);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/CreateGroupDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/CreateGroupDiscussionEndpoint.cs
new file mode 100644
index 0000000..b5a0008
--- /dev/null
+++ b/Knots/Endpoints/Discussion/CreateGroupDiscussionEndpoint.cs
@@ -0,0 +1,91 @@
+using FastEndpoints;
+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, EncryptionService encryption) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/discussions/group");
+ }
+
+ public override async Task HandleAsync(CreateGroupDiscussionRequest req, CancellationToken ct)
+ {
+ int currentUserId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
+
+ if (req.Usernames == null || req.Usernames.Count == 0)
+ {
+ await SendErrorsAsync(400, ct);
+ return;
+ }
+
+ List targets = await db.Users
+ .Where(u => req.Usernames.Contains(u.Username!))
+ .ToListAsync(ct);
+
+ if (targets.Count != req.Usernames.Count)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ if (targets.Any(t => t.Id == currentUserId))
+ {
+ await SendErrorsAsync(400, ct);
+ return;
+ }
+
+ int totalMembers = targets.Count + 1;
+
+ 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 })
+ .ToList()
+ };
+
+ db.Discussions.Add(discussion);
+ await db.SaveChangesAsync(ct); // discussion.Id disponible
+
+
+ Models.Group group = new()
+ {
+ Name = req.GroupName,
+ MembersAmount = totalMembers,
+ DiscussionId = discussion.Id,
+ GroupUsers = targets
+ .Select(t => new GroupUser { UserId = t.Id })
+ .Append(new GroupUser { UserId = currentUserId })
+ .ToList()
+ };
+
+ db.Groups.Add(group);
+ await db.SaveChangesAsync(ct); // group.Id disponible
+
+
+ discussion.GroupId = group.Id;
+ await db.SaveChangesAsync(ct);
+
+ await SendOkAsync(new GetDiscussionDto
+ {
+ Id = discussion.Id,
+ IsGroup = true,
+ Name = group.Name,
+ MembersCount = totalMembers
+ }, ct);
+ }
+}
+
+public class CreateGroupDiscussionRequest
+{
+ public string GroupName { get; set; } = "";
+ public List Usernames { get; set; } = [];
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/CreatePrivateDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/CreatePrivateDiscussionEndpoint.cs
new file mode 100644
index 0000000..4d1f353
--- /dev/null
+++ b/Knots/Endpoints/Discussion/CreatePrivateDiscussionEndpoint.cs
@@ -0,0 +1,85 @@
+using FastEndpoints;
+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, EncryptionService encryption)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/discussions/private");
+ }
+
+ public override async Task HandleAsync(CreatePrivateDiscussionRequest req, CancellationToken ct)
+ {
+ int currentUserId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
+
+ // 1. retrouver l'utilisateur cible par son nom
+ Models.User? target = await db.Users
+ .SingleOrDefaultAsync(u => u.Username == req.Username, ct);
+
+ if (target is null)
+ {
+ await SendNotFoundAsync(ct); // utilisateur introuvable
+ return;
+ }
+
+ if (target.Id == currentUserId)
+ {
+ await SendErrorsAsync(400, ct); // pas de discussion avec soi-même
+ return;
+ }
+
+ // 2. vérifier qu'une discussion privée entre les deux n'existe pas déjà
+ Models.Discussion? existing = await db.Discussions
+ .Where(d => d.GroupId == null
+ && d.UserDiscussions.Any(ud => ud.UserId == currentUserId)
+ && d.UserDiscussions.Any(ud => ud.UserId == target.Id))
+ .FirstOrDefaultAsync(ct);
+
+ if (existing is not null)
+ {
+ await SendOkAsync(new GetDiscussionDto
+ {
+ Id = existing.Id,
+ IsGroup = false,
+ Name = target.Username!,
+ MembersCount = null
+ }, ct);
+ return;
+ }
+
+ // 3. créer la discussion + les deux participants
+ Models.Discussion discussion = new()
+ {
+ IsGroup = false,
+ Key = new Models.Key { EnKey = encryption.GenerateKey() },
+ UserDiscussions =
+ [
+ new UserDiscussion { UserId = currentUserId },
+ new UserDiscussion { UserId = target.Id }
+ ]
+ };
+
+ db.Discussions.Add(discussion);
+ await db.SaveChangesAsync(ct);
+
+ await SendOkAsync(new GetDiscussionDto
+ {
+ Id = discussion.Id,
+ IsGroup = false,
+ Name = target.Username!,
+ MembersCount = null
+ }, ct);
+ }
+}
+
+public class CreatePrivateDiscussionRequest
+{
+ public string Username { get; set; } = "";
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/DeleteDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/DeleteDiscussionEndpoint.cs
new file mode 100644
index 0000000..209602d
--- /dev/null
+++ b/Knots/Endpoints/Discussion/DeleteDiscussionEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Discussion;
+
+namespace Knots.Endpoints.Discussion;
+
+public class DeleteDiscussionEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/discussions");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DeleteDiscussionDto req, CancellationToken ct)
+ {
+ Models.Discussion? discussion = mapper.Map(req);
+ db.Discussions.Remove(discussion);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/GetDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/GetDiscussionEndpoint.cs
new file mode 100644
index 0000000..915e120
--- /dev/null
+++ b/Knots/Endpoints/Discussion/GetDiscussionEndpoint.cs
@@ -0,0 +1,29 @@
+using Knots.DTO.Discussion;
+using Knots.DTO.Key;
+using Microsoft.EntityFrameworkCore;
+using FastEndpoints;
+
+namespace Knots.Endpoints.Discussion;
+
+public class GetDiscussionEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/discussions");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetDiscussionDto req, CancellationToken ct)
+ {
+ Models.Discussion? databaseDiscussion = await db.Discussions.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseDiscussion == null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ var keyDto = mapper.Map(databaseDiscussion);
+ await SendOkAsync(keyDto, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/GetDiscussionMembersEndpoint.cs b/Knots/Endpoints/Discussion/GetDiscussionMembersEndpoint.cs
new file mode 100644
index 0000000..9adf1a1
--- /dev/null
+++ b/Knots/Endpoints/Discussion/GetDiscussionMembersEndpoint.cs
@@ -0,0 +1,35 @@
+using System.Security.Claims;
+using FastEndpoints;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Discussion;
+
+public class GetDiscussionMembersEndpoint(KnotsDbContext db) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/discussions/{discussionId}/members");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ int discussionId = Route("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 members = discussion.UserDiscussions
+ .Select(ud => ud.User.Username!)
+ .ToList();
+
+ await SendOkAsync(members, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/GetDiscussionMembersWithRolesEndpoint.cs b/Knots/Endpoints/Discussion/GetDiscussionMembersWithRolesEndpoint.cs
new file mode 100644
index 0000000..456eb27
--- /dev/null
+++ b/Knots/Endpoints/Discussion/GetDiscussionMembersWithRolesEndpoint.cs
@@ -0,0 +1,52 @@
+using FastEndpoints;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Discussion;
+
+public class GetDiscussionMembersWithRolesEndpoint(KnotsDbContext db) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/discussions/{discussionId}/members/roles");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ int discussionId = Route("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 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; }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Discussion/GetMyDiscussionEndpoint.cs b/Knots/Endpoints/Discussion/GetMyDiscussionEndpoint.cs
new file mode 100644
index 0000000..a196661
--- /dev/null
+++ b/Knots/Endpoints/Discussion/GetMyDiscussionEndpoint.cs
@@ -0,0 +1,50 @@
+using System.Security.Claims;
+using FastEndpoints;
+using Knots.DTO.Discussion;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Discussion;
+
+public class GetMyDiscussionEndpoint(KnotsDbContext db) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/discussions/my");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ int userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
+
+ // Discussions privées : l'utilisateur est l'un des participants
+ IQueryable privees = db.Discussions
+ .Where(d => d.GroupId == null && d.UserDiscussions.Any(ud => ud.UserId == userId))
+ .Select(d => new GetDiscussionDto
+ {
+ Id = d.Id,
+ IsGroup = false,
+ Name = d.UserDiscussions
+ .Where(ud => ud.UserId != userId)
+ .Select(ud => ud.User.Username)
+ .FirstOrDefault() ?? "",
+ MembersCount = null,
+ GroupId = null
+ });
+
+ // Discussions de groupe : l'utilisateur est membre du groupe
+ IQueryable groupes = db.Discussions
+ .Where(d => d.Group != null && d.Group.GroupUsers.Any(gu => gu.UserId == userId))
+ .Select(d => new GetDiscussionDto
+ {
+ Id = d.Id,
+ IsGroup = true,
+ Name = d.Group!.Name!,
+ MembersCount = d.Group.MembersAmount,
+ GroupId = d.Group.Id
+ });
+
+ List discussions = await privees.Concat(groupes).ToListAsync(ct);
+
+ await SendOkAsync(discussions, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/CreateGroupEndpoint.cs b/Knots/Endpoints/Group/CreateGroupEndpoint.cs
new file mode 100644
index 0000000..cfe9050
--- /dev/null
+++ b/Knots/Endpoints/Group/CreateGroupEndpoint.cs
@@ -0,0 +1,21 @@
+using Knots.DTO.Group;
+using FastEndpoints;
+
+namespace Knots.Endpoints.Group;
+
+public class CreateGroupEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/groups");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateGroupDto req, CancellationToken ct)
+ {
+ Models.Group? group = mapper.Map(req);
+ db.Groups.Add(group);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/DeleteGroupEndpoint.cs b/Knots/Endpoints/Group/DeleteGroupEndpoint.cs
new file mode 100644
index 0000000..31d7cdf
--- /dev/null
+++ b/Knots/Endpoints/Group/DeleteGroupEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Group;
+
+namespace Knots.Endpoints.Group;
+
+public class DeleteGroupEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/groups");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DeleteGroupDto req, CancellationToken ct)
+ {
+ Models.Group? group = mapper.Map(req);
+ db.Groups.Remove(group);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/GetGroupEndpoint.cs b/Knots/Endpoints/Group/GetGroupEndpoint.cs
new file mode 100644
index 0000000..ce2aeeb
--- /dev/null
+++ b/Knots/Endpoints/Group/GetGroupEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Group;
+using Knots.DTO.Key;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Group;
+
+public class GetGroupEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/groups");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetGroupDetailsDto req, CancellationToken ct)
+ {
+ Models.Group? databaseGroup = await db.Groups.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseGroup == null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ var keyDto = mapper.Map(databaseGroup);
+ await SendOkAsync(keyDto, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/PatchGroupMembersAmountEndpoint.cs b/Knots/Endpoints/Group/PatchGroupMembersAmountEndpoint.cs
new file mode 100644
index 0000000..1a477f6
--- /dev/null
+++ b/Knots/Endpoints/Group/PatchGroupMembersAmountEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Group;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Group;
+
+public class PatchGroupMembersAmountEndpoint(KnotsDbContext knotsDbContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/groups/{@Id}/membersAmount/", x => new {x.Id});
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateGroupMembersAmountDto req, CancellationToken ct)
+ {
+ Models.Group? databaseGroup = await knotsDbContext.Groups.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseGroup is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ databaseGroup.MembersAmount = req.MembersAmount;
+ await knotsDbContext.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/PatchGroupNameEndpoint.cs b/Knots/Endpoints/Group/PatchGroupNameEndpoint.cs
new file mode 100644
index 0000000..fe54c13
--- /dev/null
+++ b/Knots/Endpoints/Group/PatchGroupNameEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Group;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Group;
+
+public class PatchGroupNameEndpoint(KnotsDbContext knotsDbContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/groups/{@Id}/name/", x => new {x.Id});
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateGroupNameDto req, CancellationToken ct)
+ {
+ Models.Group? databaseGroup = await knotsDbContext.Groups.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseGroup is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ databaseGroup.Name = req.Name;
+ await knotsDbContext.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Group/PatchGroupProfilePictureEndpoint.cs b/Knots/Endpoints/Group/PatchGroupProfilePictureEndpoint.cs
new file mode 100644
index 0000000..c970c8c
--- /dev/null
+++ b/Knots/Endpoints/Group/PatchGroupProfilePictureEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Group;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Group;
+
+public class PatchGroupProfilePictureEndpoint(KnotsDbContext knotsDbContext) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/groups/{@Id}/profilePicture/", x => new {x.Id});
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateGroupProfilePictureDto req, CancellationToken ct)
+ {
+ Models.Group? databaseGroup = await knotsDbContext.Groups.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseGroup is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ databaseGroup.ProfilePicture = req.ProfilePicture;
+ await knotsDbContext.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Key/CreateKeyEndpoint.cs b/Knots/Endpoints/Key/CreateKeyEndpoint.cs
new file mode 100644
index 0000000..66696ed
--- /dev/null
+++ b/Knots/Endpoints/Key/CreateKeyEndpoint.cs
@@ -0,0 +1,21 @@
+using Knots.DTO.Key;
+using FastEndpoints;
+
+namespace Knots.Endpoints.Key;
+
+public class CreateKeyEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/keys");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateKeyDto req, CancellationToken ct)
+ {
+ Models.Key? key = mapper.Map(req);
+ db.Keys.Add(key);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Key/DeleteKeyEndpoint.cs b/Knots/Endpoints/Key/DeleteKeyEndpoint.cs
new file mode 100644
index 0000000..9435133
--- /dev/null
+++ b/Knots/Endpoints/Key/DeleteKeyEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Key;
+
+namespace Knots.Endpoints.Key;
+
+public class DeleteKeyEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/keys");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DeleteKeyDto req, CancellationToken ct)
+ {
+ Models.Key? key = mapper.Map(req);
+ db.Keys.Remove(key);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Key/GetKeyEndpoint.cs b/Knots/Endpoints/Key/GetKeyEndpoint.cs
new file mode 100644
index 0000000..c5a4dc6
--- /dev/null
+++ b/Knots/Endpoints/Key/GetKeyEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Key;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Key;
+
+public class GetKeyEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/keys/{@Id}");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetKeyDetailsDto req, CancellationToken ct)
+ {
+ Models.Key? databaseKey = await db.Keys.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseKey == null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ var keyDto = mapper.Map(databaseKey);
+ await SendOkAsync(keyDto, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Message/CreateMessageEndpoint.cs b/Knots/Endpoints/Message/CreateMessageEndpoint.cs
new file mode 100644
index 0000000..6d7f285
--- /dev/null
+++ b/Knots/Endpoints/Message/CreateMessageEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Message;
+
+namespace Knots.Endpoints.Message;
+
+public class CreateMessageEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/messages");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateMessageDto req, CancellationToken ct)
+ {
+ Models.Message? message = mapper.Map(req);
+ db.Messages.Add(message);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Message/DeleteMessageEndpoint.cs b/Knots/Endpoints/Message/DeleteMessageEndpoint.cs
new file mode 100644
index 0000000..896dacb
--- /dev/null
+++ b/Knots/Endpoints/Message/DeleteMessageEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Message;
+
+namespace Knots.Endpoints.Message;
+
+public class DeleteMessageEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/messages");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DeleteMessageDto req, CancellationToken ct)
+ {
+ Models.Message? message = mapper.Map(req);
+ db.Messages.Remove(message);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Message/GetMessageEndpoint.cs b/Knots/Endpoints/Message/GetMessageEndpoint.cs
new file mode 100644
index 0000000..2fab57a
--- /dev/null
+++ b/Knots/Endpoints/Message/GetMessageEndpoint.cs
@@ -0,0 +1,62 @@
+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, EncryptionService encryption) : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/discussions/{DiscussionId}/messages");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetDiscussionMessagesRequest req, CancellationToken ct)
+ {
+ int userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
+
+ // l'utilisateur participe-t-il à cette discussion (privée ou via le groupe) ?
+ bool autorise = await db.Discussions
+ .Where(d => d.Id == req.DiscussionId)
+ .AnyAsync(d =>
+ d.UserDiscussions.Any(ud => ud.UserId == userId) ||
+ (d.Group != null && d.Group.GroupUsers.Any(gu => gu.UserId == userId)), ct);
+
+ if (!autorise)
+ {
+ await SendForbiddenAsync(ct);
+ return;
+ }
+
+ 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 { m.Id, m.Contenu, m.Date, m.UserId, AuthorName = m.User.Username! })
+ .ToListAsync(ct);
+
+ List 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);
+ }
+}
+
+public class GetDiscussionMessagesRequest
+{
+ public int DiscussionId { get; set; }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Role/AssignRoleEndpoint.cs b/Knots/Endpoints/Role/AssignRoleEndpoint.cs
new file mode 100644
index 0000000..1f93e4e
--- /dev/null
+++ b/Knots/Endpoints/Role/AssignRoleEndpoint.cs
@@ -0,0 +1,45 @@
+using FastEndpoints;
+using Knots.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Role;
+
+public class AssignRoleEndpoint(KnotsDbContext db) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/groups/{groupId}/members/{userId}/role");
+ }
+
+ public override async Task HandleAsync(AssignRoleRequest req, CancellationToken ct)
+ {
+ int groupId = Route("groupId");
+ int userId = Route("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; }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Role/CreateRoleEndpoint.cs b/Knots/Endpoints/Role/CreateRoleEndpoint.cs
new file mode 100644
index 0000000..777ccf5
--- /dev/null
+++ b/Knots/Endpoints/Role/CreateRoleEndpoint.cs
@@ -0,0 +1,32 @@
+using FastEndpoints;
+using Knots.Models;
+
+namespace Knots.Endpoints.Role;
+
+public class CreateRoleEndpoint(KnotsDbContext db) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/roles");
+ }
+
+ public override async Task HandleAsync(CreateRoleRequest req, CancellationToken ct)
+ {
+ Models.Role role = new() { Libelle = req.Libelle };
+ db.Roles.Add(role);
+ await db.SaveChangesAsync(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; } = "";
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Role/DeleteRoleEndpoint.cs b/Knots/Endpoints/Role/DeleteRoleEndpoint.cs
new file mode 100644
index 0000000..bf560f1
--- /dev/null
+++ b/Knots/Endpoints/Role/DeleteRoleEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.Role;
+
+namespace Knots.Endpoints.Role;
+
+public class DeleteRoleEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/roles");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(DeleteRoleDto req, CancellationToken ct)
+ {
+ Models.Role? role = mapper.Map(req);
+ db.Roles.Remove(role);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/Role/GetRoleEndpoint.cs b/Knots/Endpoints/Role/GetRoleEndpoint.cs
new file mode 100644
index 0000000..f1daa46
--- /dev/null
+++ b/Knots/Endpoints/Role/GetRoleEndpoint.cs
@@ -0,0 +1,29 @@
+using FastEndpoints;
+using Knots.DTO.Role;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.Role;
+
+public class GetRoleEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Get ("/roles/{@Id}", x => new { x.Libelle });
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetRoleDto req, CancellationToken ct)
+ {
+ Models.Role? databaseRole = await db.Roles.SingleOrDefaultAsync(x => x.Libelle == req.Libelle, cancellationToken: ct);
+
+ if (databaseRole == null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ var roleDto = mapper.Map(databaseRole);
+ await SendOkAsync(roleDto, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/CreateUserEndpoint.cs b/Knots/Endpoints/User/CreateUserEndpoint.cs
new file mode 100644
index 0000000..c5bc018
--- /dev/null
+++ b/Knots/Endpoints/User/CreateUserEndpoint.cs
@@ -0,0 +1,33 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class CreateUserEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/users");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CreateUserDto req, CancellationToken ct)
+ {
+ bool usernameExists = await db.Users
+ .AnyAsync(x => x.Username == req.Username, cancellationToken: ct);
+
+ if (usernameExists)
+ {
+ AddError(x => x.Username, "Ce nom d'utilisateur est déjà pris.");
+ await SendErrorsAsync(cancellation: ct);
+ return;
+ }
+
+ req.Password = BCrypt.Net.BCrypt.HashPassword(req.Password);
+ Models.User? user = mapper.Map(req);
+ db.Users.Add(user);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/DeleteUserEndpoint.cs b/Knots/Endpoints/User/DeleteUserEndpoint.cs
new file mode 100644
index 0000000..1fb7cb9
--- /dev/null
+++ b/Knots/Endpoints/User/DeleteUserEndpoint.cs
@@ -0,0 +1,21 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class DeleteUserEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Delete ("/users/{@Id}");
+ }
+
+ public override async Task HandleAsync(DeleteUserDto req, CancellationToken ct)
+ {
+ Models.User? user = mapper.Map(req);
+ db.Users.Add(user);
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/GetAllUsersEndpoint.cs b/Knots/Endpoints/User/GetAllUsersEndpoint.cs
new file mode 100644
index 0000000..92dfea2
--- /dev/null
+++ b/Knots/Endpoints/User/GetAllUsersEndpoint.cs
@@ -0,0 +1,24 @@
+using AutoMapper.QueryableExtensions;
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class GetAllUsersEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : EndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get ("/users");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var users = await db.Users
+ .ProjectTo(mapper.ConfigurationProvider)
+ .ToListAsync(ct);
+
+ await SendOkAsync(users, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/GetUserEndpoint.cs b/Knots/Endpoints/User/GetUserEndpoint.cs
new file mode 100644
index 0000000..08de2ba
--- /dev/null
+++ b/Knots/Endpoints/User/GetUserEndpoint.cs
@@ -0,0 +1,28 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class GetUserEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Get ("/users/{@Id}", x => new { x.Username });
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetUserDetailsDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Username == req.Username, cancellationToken: ct);
+
+ if (databaseUser == null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ var userDto = mapper.Map(databaseUser);
+ await SendOkAsync(userDto, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/LoginEndpoint.cs b/Knots/Endpoints/User/LoginEndpoint.cs
new file mode 100644
index 0000000..87f20f3
--- /dev/null
+++ b/Knots/Endpoints/User/LoginEndpoint.cs
@@ -0,0 +1,40 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Knots.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class LoginEndpoint(KnotsDbContext db, JwtService jwtService) : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/users/login");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(LoginUserDto req, CancellationToken ct)
+ {
+ Models.User? user = await db.Users
+ .SingleOrDefaultAsync(x => x.Username == req.Username, cancellationToken: ct);
+
+ if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.Password)) // hash à ajouter plus tard
+ {
+ await SendUnauthorizedAsync(ct);
+ return;
+ }
+
+ string token = jwtService.GenerateToken(user);
+
+ await SendOkAsync(new LoginResponseDto
+ {
+ Token = token,
+ Id = user.Id,
+ Username = user.Username!,
+ Email = user.Email,
+ Tel = user.Tel,
+ ProfilePicture = user.ProfilePicture,
+ Description = user.Description
+ }, ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/PatchUserContactEndpoint.cs b/Knots/Endpoints/User/PatchUserContactEndpoint.cs
new file mode 100644
index 0000000..4cc1e92
--- /dev/null
+++ b/Knots/Endpoints/User/PatchUserContactEndpoint.cs
@@ -0,0 +1,47 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class PatchUserContactEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/users/{@Id}/contact/", x => new {x.Id});
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateUserContactDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseUser is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ if (databaseUser.Email != req.Email)
+ {
+ databaseUser.Email = req.Email;
+ }
+ else
+ {
+ databaseUser.Email = databaseUser.Email;
+ }
+
+ if (databaseUser.Tel != req.Tel)
+ {
+ databaseUser.Tel = req.Tel;
+ }else
+ {
+ databaseUser.Tel = databaseUser.Tel;
+ }
+
+ mapper.Map(req, databaseUser);
+
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/PatchUserDescriptionEndpoint.cs b/Knots/Endpoints/User/PatchUserDescriptionEndpoint.cs
new file mode 100644
index 0000000..0b4d770
--- /dev/null
+++ b/Knots/Endpoints/User/PatchUserDescriptionEndpoint.cs
@@ -0,0 +1,30 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class PatchUserDescriptionEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/users/{@Id}/description/");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateUserDescriptionDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseUser is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ mapper.Map(req, databaseUser);
+
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/PatchUserPasswordEndpoint.cs b/Knots/Endpoints/User/PatchUserPasswordEndpoint.cs
new file mode 100644
index 0000000..3aedb60
--- /dev/null
+++ b/Knots/Endpoints/User/PatchUserPasswordEndpoint.cs
@@ -0,0 +1,30 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class PatchUserPasswordEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/users/{@Id}/password/", x => new {x.Id});
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateUserPasswordDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseUser is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ mapper.Map(req, databaseUser);
+
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/PatchUserProfilePictureEndpoint.cs b/Knots/Endpoints/User/PatchUserProfilePictureEndpoint.cs
new file mode 100644
index 0000000..6b17681
--- /dev/null
+++ b/Knots/Endpoints/User/PatchUserProfilePictureEndpoint.cs
@@ -0,0 +1,30 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+public class PatchUserProfilePictureEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/users/{@Id}/profilepicture/");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateUserProfilePictureDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseUser is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ mapper.Map(req, databaseUser);
+
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Endpoints/User/PatchUsernameEndpoint.cs b/Knots/Endpoints/User/PatchUsernameEndpoint.cs
new file mode 100644
index 0000000..9148131
--- /dev/null
+++ b/Knots/Endpoints/User/PatchUsernameEndpoint.cs
@@ -0,0 +1,41 @@
+using FastEndpoints;
+using Knots.DTO.User;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots.Endpoints.User;
+
+
+public class PatchUsernameEndpoint(KnotsDbContext db, AutoMapper.IMapper mapper) : Endpoint
+{
+ public override void Configure()
+ {
+ Patch("/users/{@Id}/username/");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(UpdateUsernameDto req, CancellationToken ct)
+ {
+ Models.User? databaseUser = await db.Users.SingleOrDefaultAsync(x => x.Id == req.Id, cancellationToken: ct);
+
+ if (databaseUser is null)
+ {
+ await SendNotFoundAsync(ct);
+ return;
+ }
+
+ bool usernameExists = await db.Users
+ .AnyAsync(x => x.Username == req.Username && x.Id != req.Id, cancellationToken: ct);
+
+ if (usernameExists)
+ {
+ AddError(x => x.Username, "Ce nom d'utilisateur est déjà pris.");
+ await SendErrorsAsync(cancellation: ct);
+ return;
+ }
+
+ mapper.Map(req, databaseUser);
+
+ await db.SaveChangesAsync(ct);
+ await SendNoContentAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Hubs/ChatHub.cs b/Knots/Hubs/ChatHub.cs
new file mode 100644
index 0000000..52c3d25
--- /dev/null
+++ b/Knots/Hubs/ChatHub.cs
@@ -0,0 +1,60 @@
+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, EncryptionService encryption) : Hub
+{
+ // Rejoindre une conversation (room)
+ public async Task JoinConversation(string discussionId)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, discussionId);
+ }
+
+ // Quitter une conversation
+ public async Task LeaveConversation(string discussionId)
+ {
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, discussionId);
+ }
+
+ // Envoyer un message à une conversation
+ 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
+ {
+ 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();
+
+ // 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 discussionId)
+ {
+ await Clients.OthersInGroup(discussionId)
+ .SendAsync("UserTyping", Context.UserIdentifier);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Knots.csproj b/Knots/Knots.csproj
index 000441f..d6573f2 100644
--- a/Knots/Knots.csproj
+++ b/Knots/Knots.csproj
@@ -1,19 +1,32 @@
- net9.0
+ net8.0
enable
enable
-
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
-
-
+
diff --git a/Knots/KnotsDbContext.cs b/Knots/KnotsDbContext.cs
new file mode 100644
index 0000000..fa5cf36
--- /dev/null
+++ b/Knots/KnotsDbContext.cs
@@ -0,0 +1,79 @@
+using Knots.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Knots;
+
+public class KnotsDbContext : DbContext
+{
+ public DbSet Users => Set();
+ public DbSet Discussions => Set();
+ public DbSet Groups => Set();
+ public DbSet Messages => Set();
+ public DbSet Roles => Set();
+ public DbSet Keys => Set();
+ public DbSet UserDiscussions => Set();
+ public DbSet GroupUsers => Set();
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ //Infos de connexion à la base de données
+ string connectionString =
+ "Server=romaric-thibault.fr;" +
+ "Database=knots;" +
+ "User Id=knots;" +
+ "Password=knots;" +
+ "TrustServerCertificate=true;";
+
+ optionsBuilder.UseSqlServer(connectionString);
+ }
+
+protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasKey(gu => new { gu.GroupId, gu.UserId });
+
+ modelBuilder.Entity()
+ .HasOne(gu => gu.Group)
+ .WithMany(g => g.GroupUsers)
+ .HasForeignKey(gu => gu.GroupId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity()
+ .HasOne(gu => gu.User)
+ .WithMany()
+ .HasForeignKey(gu => gu.UserId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ modelBuilder.Entity()
+ .HasOne(gu => gu.Role)
+ .WithMany(r => r.GroupUsers)
+ .HasForeignKey(gu => gu.RoleId);
+
+ modelBuilder.Entity()
+ .HasOne(d => d.Group)
+ .WithMany()
+ .HasForeignKey(d => d.GroupId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ modelBuilder.Entity()
+ .HasOne(g => g.Discussion)
+ .WithMany()
+ .HasForeignKey(g => g.DiscussionId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ modelBuilder.Entity()
+ .HasKey(ud => new { ud.UserId, ud.DiscussionId });
+
+ modelBuilder.Entity()
+ .HasOne(ud => ud.Discussion)
+ .WithMany(d => d.UserDiscussions)
+ .HasForeignKey(ud => ud.DiscussionId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity()
+ .HasOne(ud => ud.User)
+ .WithMany(u => u.UserDiscussions)
+ .HasForeignKey(ud => ud.UserId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
\ No newline at end of file
diff --git a/Knots/Migrations/20260505083044_InitialDatabase.Designer.cs b/Knots/Migrations/20260505083044_InitialDatabase.Designer.cs
new file mode 100644
index 0000000..7b95bff
--- /dev/null
+++ b/Knots/Migrations/20260505083044_InitialDatabase.Designer.cs
@@ -0,0 +1,232 @@
+//
+using System;
+using Knots;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ [DbContext(typeof(KnotsDbContext))]
+ [Migration("20260505083044_InitialDatabase")]
+ partial class InitialDatabase
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.25")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Discussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.Property("MembersAmount")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EnKey")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Keys");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Contenu")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Date")
+ .HasColumnType("datetime2");
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.Property("Type")
+ .HasColumnType("bit");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("KeyId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Libelle")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Roles");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(70)
+ .HasColumnType("nvarchar(70)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Tel")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", null)
+ .WithMany("Messages")
+ .HasForeignKey("DiscussionId");
+
+ b.HasOne("Knots.Models.Group", "Group")
+ .WithMany()
+ .HasForeignKey("GroupId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.Key", "Key")
+ .WithMany()
+ .HasForeignKey("KeyId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany("Messages")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Group");
+
+ b.Navigation("Key");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Navigation("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Navigation("Messages");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Knots/Migrations/20260505083044_InitialDatabase.cs b/Knots/Migrations/20260505083044_InitialDatabase.cs
new file mode 100644
index 0000000..96741b9
--- /dev/null
+++ b/Knots/Migrations/20260505083044_InitialDatabase.cs
@@ -0,0 +1,172 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ ///
+ public partial class InitialDatabase : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Discussions",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ KeyId = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Discussions", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Groups",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
+ MembersAmount = table.Column(type: "int", nullable: false),
+ ProfilePicture = table.Column(type: "nvarchar(max)", nullable: true),
+ KeyId = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Groups", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Keys",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ EnKey = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Keys", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Roles",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Libelle = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Roles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Username = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
+ Description = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true),
+ Password = table.Column(type: "nvarchar(max)", nullable: false),
+ Email = table.Column(type: "nvarchar(70)", maxLength: 70, nullable: false),
+ Tel = table.Column(type: "nvarchar(max)", nullable: false),
+ ProfilePicture = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Messages",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Contenu = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
+ Date = table.Column(type: "datetime2", nullable: false),
+ Type = table.Column(type: "bit", nullable: false),
+ GroupId = table.Column(type: "int", nullable: false),
+ KeyId = table.Column(type: "int", nullable: false),
+ UserId = table.Column(type: "int", nullable: false),
+ DiscussionId = table.Column(type: "int", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Messages", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Messages_Discussions_DiscussionId",
+ column: x => x.DiscussionId,
+ principalTable: "Discussions",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_Messages_Groups_GroupId",
+ column: x => x.GroupId,
+ principalTable: "Groups",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Messages_Keys_KeyId",
+ column: x => x.KeyId,
+ principalTable: "Keys",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Messages_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Messages_DiscussionId",
+ table: "Messages",
+ column: "DiscussionId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Messages_GroupId",
+ table: "Messages",
+ column: "GroupId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Messages_KeyId",
+ table: "Messages",
+ column: "KeyId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Messages_UserId",
+ table: "Messages",
+ column: "UserId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Messages");
+
+ migrationBuilder.DropTable(
+ name: "Roles");
+
+ migrationBuilder.DropTable(
+ name: "Discussions");
+
+ migrationBuilder.DropTable(
+ name: "Groups");
+
+ migrationBuilder.DropTable(
+ name: "Keys");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/Knots/Migrations/20260610135459_AddRoleIdToUser.Designer.cs b/Knots/Migrations/20260610135459_AddRoleIdToUser.Designer.cs
new file mode 100644
index 0000000..16a3140
--- /dev/null
+++ b/Knots/Migrations/20260610135459_AddRoleIdToUser.Designer.cs
@@ -0,0 +1,316 @@
+//
+using System;
+using Knots;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ [DbContext(typeof(KnotsDbContext))]
+ [Migration("20260610135459_AddRoleIdToUser")]
+ partial class AddRoleIdToUser
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.25")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsGroup")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.ToTable("Discussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.Property("MembersAmount")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId")
+ .IsUnique();
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EnKey")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Keys");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Contenu")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Date")
+ .HasColumnType("datetime2");
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.Property("Type")
+ .HasColumnType("bit");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("KeyId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Libelle")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Roles");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(70)
+ .HasColumnType("nvarchar(70)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.Property("Tel")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Knots.Models.UserDiscussion", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.HasKey("UserId", "DiscussionId");
+
+ b.HasIndex("DiscussionId");
+
+ b.ToTable("UserDiscussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithOne("Group")
+ .HasForeignKey("Knots.Models.Group", "DiscussionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithMany("Messages")
+ .HasForeignKey("DiscussionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.Group", "Group")
+ .WithMany()
+ .HasForeignKey("GroupId");
+
+ b.HasOne("Knots.Models.Key", "Key")
+ .WithMany("Messages")
+ .HasForeignKey("KeyId");
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany("Messages")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+
+ b.Navigation("Group");
+
+ b.Navigation("Key");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.HasOne("Knots.Models.Role", "Role")
+ .WithMany("Users")
+ .HasForeignKey("RoleId");
+
+ b.Navigation("Role");
+ });
+
+ modelBuilder.Entity("Knots.Models.UserDiscussion", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithMany("UserDiscussions")
+ .HasForeignKey("DiscussionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany("UserDiscussions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Navigation("Group");
+
+ b.Navigation("Messages");
+
+ b.Navigation("UserDiscussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Navigation("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.Role", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Navigation("Messages");
+
+ b.Navigation("UserDiscussions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Knots/Migrations/20260610135459_AddRoleIdToUser.cs b/Knots/Migrations/20260610135459_AddRoleIdToUser.cs
new file mode 100644
index 0000000..3e62fc4
--- /dev/null
+++ b/Knots/Migrations/20260610135459_AddRoleIdToUser.cs
@@ -0,0 +1,270 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ ///
+ public partial class AddRoleIdToUser : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Discussions_DiscussionId",
+ table: "Messages");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Groups_GroupId",
+ table: "Messages");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Keys_KeyId",
+ table: "Messages");
+
+ migrationBuilder.DropColumn(
+ name: "KeyId",
+ table: "Discussions");
+
+ migrationBuilder.AddColumn(
+ name: "RoleId",
+ table: "Users",
+ type: "int",
+ nullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "KeyId",
+ table: "Messages",
+ type: "int",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "int");
+
+ migrationBuilder.AlterColumn(
+ name: "GroupId",
+ table: "Messages",
+ type: "int",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "int");
+
+ migrationBuilder.AlterColumn(
+ name: "DiscussionId",
+ table: "Messages",
+ type: "int",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "int",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "DiscussionId",
+ table: "Groups",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "CreatedAt",
+ table: "Discussions",
+ type: "datetime2",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "IsGroup",
+ table: "Discussions",
+ type: "bit",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.CreateTable(
+ name: "UserDiscussions",
+ columns: table => new
+ {
+ UserId = table.Column(type: "int", nullable: false),
+ DiscussionId = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_UserDiscussions", x => new { x.UserId, x.DiscussionId });
+ table.ForeignKey(
+ name: "FK_UserDiscussions_Discussions_DiscussionId",
+ column: x => x.DiscussionId,
+ principalTable: "Discussions",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_UserDiscussions_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_RoleId",
+ table: "Users",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Groups_DiscussionId",
+ table: "Groups",
+ column: "DiscussionId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserDiscussions_DiscussionId",
+ table: "UserDiscussions",
+ column: "DiscussionId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Groups_Discussions_DiscussionId",
+ table: "Groups",
+ column: "DiscussionId",
+ principalTable: "Discussions",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Discussions_DiscussionId",
+ table: "Messages",
+ column: "DiscussionId",
+ principalTable: "Discussions",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Groups_GroupId",
+ table: "Messages",
+ column: "GroupId",
+ principalTable: "Groups",
+ principalColumn: "Id");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Keys_KeyId",
+ table: "Messages",
+ column: "KeyId",
+ principalTable: "Keys",
+ principalColumn: "Id");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Users_Roles_RoleId",
+ table: "Users",
+ column: "RoleId",
+ principalTable: "Roles",
+ principalColumn: "Id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Groups_Discussions_DiscussionId",
+ table: "Groups");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Discussions_DiscussionId",
+ table: "Messages");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Groups_GroupId",
+ table: "Messages");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Messages_Keys_KeyId",
+ table: "Messages");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Users_Roles_RoleId",
+ table: "Users");
+
+ migrationBuilder.DropTable(
+ name: "UserDiscussions");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Users_RoleId",
+ table: "Users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Groups_DiscussionId",
+ table: "Groups");
+
+ migrationBuilder.DropColumn(
+ name: "RoleId",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "DiscussionId",
+ table: "Groups");
+
+ migrationBuilder.DropColumn(
+ name: "CreatedAt",
+ table: "Discussions");
+
+ migrationBuilder.DropColumn(
+ name: "IsGroup",
+ table: "Discussions");
+
+ migrationBuilder.AlterColumn(
+ name: "KeyId",
+ table: "Messages",
+ type: "int",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "int",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "GroupId",
+ table: "Messages",
+ type: "int",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "int",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "DiscussionId",
+ table: "Messages",
+ type: "int",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "int");
+
+ migrationBuilder.AddColumn(
+ name: "KeyId",
+ table: "Discussions",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Discussions_DiscussionId",
+ table: "Messages",
+ column: "DiscussionId",
+ principalTable: "Discussions",
+ principalColumn: "Id");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Groups_GroupId",
+ table: "Messages",
+ column: "GroupId",
+ principalTable: "Groups",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Messages_Keys_KeyId",
+ table: "Messages",
+ column: "KeyId",
+ principalTable: "Keys",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/Knots/Migrations/20260610224937_FixGroupDiscussion.Designer.cs b/Knots/Migrations/20260610224937_FixGroupDiscussion.Designer.cs
new file mode 100644
index 0000000..ddfe896
--- /dev/null
+++ b/Knots/Migrations/20260610224937_FixGroupDiscussion.Designer.cs
@@ -0,0 +1,380 @@
+//
+using System;
+using Knots;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ [DbContext(typeof(KnotsDbContext))]
+ [Migration("20260610224937_FixGroupDiscussion")]
+ partial class FixGroupDiscussion
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.25")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("IsGroup")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId");
+
+ b.ToTable("Discussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("MembersAmount")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId");
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Knots.Models.GroupUser", b =>
+ {
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("GroupId", "UserId");
+
+ b.HasIndex("RoleId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("GroupUsers");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EnKey")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Keys");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AuthorId")
+ .HasColumnType("int");
+
+ b.Property("Contenu")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Date")
+ .HasColumnType("datetime2");
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("KeyId")
+ .HasColumnType("int");
+
+ b.Property("Type")
+ .HasColumnType("bit");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("KeyId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Libelle")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Roles");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(70)
+ .HasColumnType("nvarchar(70)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.Property("Tel")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Knots.Models.UserDiscussion", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.HasKey("UserId", "DiscussionId");
+
+ b.HasIndex("DiscussionId");
+
+ b.ToTable("UserDiscussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.HasOne("Knots.Models.Group", "Group")
+ .WithMany()
+ .HasForeignKey("GroupId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Group");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithMany()
+ .HasForeignKey("DiscussionId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+ });
+
+ modelBuilder.Entity("Knots.Models.GroupUser", b =>
+ {
+ b.HasOne("Knots.Models.Group", "Group")
+ .WithMany("GroupUsers")
+ .HasForeignKey("GroupId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.Role", "Role")
+ .WithMany("GroupUsers")
+ .HasForeignKey("RoleId");
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Group");
+
+ b.Navigation("Role");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.Message", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithMany("Messages")
+ .HasForeignKey("DiscussionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.Group", "Group")
+ .WithMany()
+ .HasForeignKey("GroupId");
+
+ b.HasOne("Knots.Models.Key", "Key")
+ .WithMany("Messages")
+ .HasForeignKey("KeyId");
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany("Messages")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+
+ b.Navigation("Group");
+
+ b.Navigation("Key");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.HasOne("Knots.Models.Role", "Role")
+ .WithMany("Users")
+ .HasForeignKey("RoleId");
+
+ b.Navigation("Role");
+ });
+
+ modelBuilder.Entity("Knots.Models.UserDiscussion", b =>
+ {
+ b.HasOne("Knots.Models.Discussion", "Discussion")
+ .WithMany("UserDiscussions")
+ .HasForeignKey("DiscussionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Knots.Models.User", "User")
+ .WithMany("UserDiscussions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Discussion");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Navigation("Messages");
+
+ b.Navigation("UserDiscussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.Navigation("GroupUsers");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Navigation("Messages");
+ });
+
+ modelBuilder.Entity("Knots.Models.Role", b =>
+ {
+ b.Navigation("GroupUsers");
+
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Knots.Models.User", b =>
+ {
+ b.Navigation("Messages");
+
+ b.Navigation("UserDiscussions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Knots/Migrations/20260610224937_FixGroupDiscussion.cs b/Knots/Migrations/20260610224937_FixGroupDiscussion.cs
new file mode 100644
index 0000000..3b64786
--- /dev/null
+++ b/Knots/Migrations/20260610224937_FixGroupDiscussion.cs
@@ -0,0 +1,21 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ ///
+ public partial class FixGroupDiscussion : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AuthorId",
+ table: "Messages",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ }
+}
diff --git a/Knots/Migrations/KnotsDbContextModelSnapshot.cs b/Knots/Migrations/KnotsDbContextModelSnapshot.cs
new file mode 100644
index 0000000..ba31ee3
--- /dev/null
+++ b/Knots/Migrations/KnotsDbContextModelSnapshot.cs
@@ -0,0 +1,377 @@
+//
+using System;
+using Knots;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Knots.Migrations
+{
+ [DbContext(typeof(KnotsDbContext))]
+ partial class KnotsDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.25")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Knots.Models.Discussion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("IsGroup")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId");
+
+ b.ToTable("Discussions");
+ });
+
+ modelBuilder.Entity("Knots.Models.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DiscussionId")
+ .HasColumnType("int");
+
+ b.Property("MembersAmount")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProfilePicture")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DiscussionId");
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Knots.Models.GroupUser", b =>
+ {
+ b.Property("GroupId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("GroupId", "UserId");
+
+ b.HasIndex("RoleId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("GroupUsers");
+ });
+
+ modelBuilder.Entity("Knots.Models.Key", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property