diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..393d187 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.30.6", + "commands": [ + "dotnet-csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/core/.env.template b/core/.env.template index 076ff64..8724981 100644 --- a/core/.env.template +++ b/core/.env.template @@ -5,9 +5,6 @@ ASPNETCORE_HTTP_PORT=8080 ASPNETCORE_ENVIRONMENT=Development CONNECTION_STRING=User ID=postgres;Password=test123;Host=host.docker.internal;Port=5432;Database=ps; -SUPABASE_PROJECT_ID= -SUPABASE_SECRET_KEY= -SUPABASE_ANON_KEY= EMAIL_SERVER=smtp.gmail.com EMAIL_PORT=587 @@ -21,6 +18,11 @@ REDIS_CONNECTION_STRING=host.docker.internal:6379,password=eYVX7EwVmmxKPCDmwMtyK CONTAINER_DIR=/app/volume CDN_URL=http://localhost:6464 +OPENID_ISSUER=https://example.com +OPENID_BASE_URL=[Insert your URL here] +OPENID_CLIENT_ID=[Insert your client id here] +OPENID_CLIENT_SECRET=[Insert your client secret here] + FRONTEND_BASE_URL=http://localhost:3000 RATE_LIMIT_TIME_WINDOW_SECONDS=10 diff --git a/core/Controllers/DraftEventsController.cs b/core/Controllers/DraftEventsController.cs index c4cd30c..b5e038b 100644 --- a/core/Controllers/DraftEventsController.cs +++ b/core/Controllers/DraftEventsController.cs @@ -28,7 +28,7 @@ namespace api.core.Controllers; [ApiController] [Authorize(Policy = AuthPolicies.OrganizerIsActive)] [Route("api/organizer-drafts")] // TODO : Change route to /api/me/drafts -public class DraftEventsController(ILogger logger, IDraftEventService draftService) : ControllerBase +public class DraftEventsController(ILogger logger, IDraftEventService draftService, IJwtUtils jwtUtils) : ControllerBase { /// /// Add a draft event to the database. This event will be saved as a draft and will not be visible @@ -41,7 +41,7 @@ public IActionResult AddDraft([FromForm] DraftEventRequestDTO draftEvent) { logger.LogInformation($"Adding new draft"); - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); var evnt = draftService.AddDraftEvent(userId, draftEvent); return new OkObjectResult( @@ -61,7 +61,7 @@ public IActionResult AddDraft([FromForm] DraftEventRequestDTO draftEvent) [HttpPatch("{id}")] // TODO: Change this to a HttpPut instead public IActionResult UpdateDraft(Guid id, [FromForm] DraftEventRequestDTO draftEvent) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); return draftService.UpdateDraftEvent(userId, id, draftEvent) ? Ok() : BadRequest(); } } diff --git a/core/Controllers/EventsController.cs b/core/Controllers/EventsController.cs index e44944e..f7ab8fe 100644 --- a/core/Controllers/EventsController.cs +++ b/core/Controllers/EventsController.cs @@ -46,7 +46,7 @@ public class EventsController( public ActionResult> GetEvents( [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate, - [FromQuery] Guid? organizerId, + [FromQuery] string? organizerId, [FromQuery] string? title, [FromQuery] IEnumerable? activityAreas, [FromQuery] IEnumerable? tags, diff --git a/core/Controllers/MeController.cs b/core/Controllers/MeController.cs index 72ec7fe..b2a1c27 100644 --- a/core/Controllers/MeController.cs +++ b/core/Controllers/MeController.cs @@ -18,10 +18,11 @@ namespace api.core.controllers; /// your JWT token. /// /// The User Service will allows managing your user's data +/// The JWT Utils allow to retrieve the ID from the user [Authorize] [ApiController] [Route("api/me")] -public class MeController(IUserService userService) : ControllerBase +public class MeController(IUserService userService, IJwtUtils jwtUtils) : ControllerBase { /// /// Get the user connected to the API using the JWT token @@ -30,8 +31,8 @@ public class MeController(IUserService userService) : ControllerBase [HttpGet] public IActionResult GetUser() { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); - var organizer = userService.GetUser(userId); + //var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers.Authorization!); + var organizer = userService.GetUser(Request.Headers.Authorization!); return new OkObjectResult( new Response @@ -48,7 +49,7 @@ public IActionResult GetUser() [HttpPatch] public IActionResult UpdateUser([FromBody] UserUpdateDTO user) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); return userService.UpdateUser(userId, user) ? Ok() : BadRequest(); } @@ -60,7 +61,7 @@ public IActionResult UpdateUser([FromBody] UserUpdateDTO user) [HttpPatch("avatar")] public IActionResult UpdateUserAvatar([FromForm] UserAvatarUpdateDTO avatarReq) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); var url = userService.UpdateUserAvatar(userId, avatarReq.avatarFile); return new OkObjectResult( diff --git a/core/Controllers/ModeratorEventsController.cs b/core/Controllers/ModeratorEventsController.cs index f0d7830..6e1e4ef 100644 --- a/core/Controllers/ModeratorEventsController.cs +++ b/core/Controllers/ModeratorEventsController.cs @@ -25,7 +25,8 @@ namespace api.core.Controllers; public class ModeratorEventsController( ILogger logger, IEventService eventService, - IUserService userService) : ControllerBase + IUserService userService, + IJwtUtils jwtUtils) : ControllerBase { /// /// Update the state of an event. This is used for a moderator that needs to @@ -41,7 +42,7 @@ public class ModeratorEventsController( [HttpPatch("{id}/state")] public IActionResult UpdateEventState(Guid id, [FromQuery] State newState, [FromQuery] string? reason) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); return eventService.UpdateEventState(userId, id, newState, reason) ? Ok() : BadRequest(); } diff --git a/core/Controllers/OrganizerEventsController.cs b/core/Controllers/OrganizerEventsController.cs index 0777c7c..1010934 100644 --- a/core/Controllers/OrganizerEventsController.cs +++ b/core/Controllers/OrganizerEventsController.cs @@ -20,7 +20,7 @@ namespace api.core.Controllers; [ApiController] [Authorize(Policy = AuthPolicies.OrganizerIsActive)] [Route("api/organizer-events")] -public class OrganizerEventsController(ILogger logger, IEventService eventService) : ControllerBase +public class OrganizerEventsController(ILogger logger, IEventService eventService, IJwtUtils jwtUtils) : ControllerBase { /// /// Fetch events for the currently connected organizer @@ -46,7 +46,7 @@ public IActionResult MyEvents( [FromQuery] State state = State.All ) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); logger.LogInformation("Getting events"); var validFilter = new PaginationRequest(pagination.PageNumber, pagination.PageSize); @@ -72,7 +72,7 @@ public IActionResult AddEvent([FromForm] EventCreationRequestDTO dto) { logger.LogInformation($"Adding new event"); - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); var evnt = eventService.AddEvent(userId, dto); return new OkObjectResult( @@ -85,7 +85,7 @@ public IActionResult AddEvent([FromForm] EventCreationRequestDTO dto) [HttpDelete("{id}")] public IActionResult DeleteEvent(Guid id) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); var isDeleted = eventService.DeleteEvent(userId, id); return isDeleted ? Ok() : BadRequest(); } @@ -93,7 +93,7 @@ public IActionResult DeleteEvent(Guid id) [HttpPatch("{id}")] public IActionResult UpdateEvent(Guid id, [FromForm] EventUpdateRequestDTO dto) { - var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); + var userId = jwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); return eventService.UpdateEvent(userId, id, dto) ? Ok() : BadRequest(); } } diff --git a/core/Controllers/OrganizersController.cs b/core/Controllers/OrganizersController.cs index 12922ec..5b27241 100644 --- a/core/Controllers/OrganizersController.cs +++ b/core/Controllers/OrganizersController.cs @@ -1,5 +1,6 @@ using api.core.data.entities; using api.core.Data; +using api.core.Data.Entities; using api.core.Data.Enums; using api.core.Data.Exceptions; using api.core.Data.requests; @@ -26,7 +27,6 @@ namespace api.core.controllers; /// for this to work. /// /// Used to fetch and manage the organizers -/// Used to create a new user in the Supabase database /// Used to send an email to the newly created organizer /// Used to fetch the FRONTEND_BASE_URL from the environments variables [Authorize(Policy = AuthPolicies.IsModerator)] @@ -34,7 +34,6 @@ namespace api.core.controllers; [Route("api/organizers")] public class ModeratorUserController( IUserService userService, - IAuthService authService, IEmailService emailService, IConfiguration configuration) : ControllerBase { @@ -49,7 +48,7 @@ public class ModeratorUserController( /// [AllowAnonymous] [HttpGet("{organizerId}")] - public IActionResult GetOrganizer(Guid organizerId) + public IActionResult GetOrganizer(string organizerId) { var user = userService.GetUser(organizerId); return user.Type == "Organizer" ? @@ -57,7 +56,7 @@ public IActionResult GetOrganizer(Guid organizerId) { Data = user }) - : throw new NotFoundException(); + : throw new NotFoundException(); } /// @@ -68,32 +67,32 @@ public IActionResult GetOrganizer(Guid organizerId) /// /// /// - [HttpPost] - public async Task CreateOrganizer([FromBody] UserCreateDTO organizer) - { - var strongPassword = GenerateRandomPassword(12); - var supabaseUser = authService.SignUp(organizer.Email, strongPassword); - _ = Guid.TryParse(supabaseUser, out Guid userId); - var created = userService.AddOrganizer(userId, organizer); - var frontBaseUrl = configuration.GetValue("FRONTEND_BASE_URL") ?? throw new ArgumentNullException("FRONTEND_BASE_URL is not set"); - await emailService.SendEmailAsync( - organizer.Email, - "Votre compte Hello!", - new UserCreationModel - { - Title = "Création de votre compte Hello!", - Salutation = $"Bonjour {organizer.Organization},", - AccountCreatedText = "Votre compte Hello a été créé!", - TemporaryPasswordHeader = "Votre mot de passe temporaire est: ", - TemporaryPassword = strongPassword, - LoginButtonText = "Se connecter", - ButtonLink = new Uri($"{frontBaseUrl}/fr/login") - }, - emails.EmailsUtils.UserCreationTemplate - ); - - return Ok(new Response { Data = created }); - } + //[HttpPost] + //public async Task CreateOrganizer([FromBody] UserCreateDTO organizer) + //{ + // var strongPassword = GenerateRandomPassword(12); + // var supabaseUser = authService.SignUp(organizer.Email, strongPassword); + // _ = Guid.TryParse(supabaseUser, out Guid userId); + // var created = userService.AddOrganizer(userId, organizer); + // var frontBaseUrl = configuration.GetValue("FRONTEND_BASE_URL") ?? throw new ArgumentNullException("FRONTEND_BASE_URL is not set"); + // await emailService.SendEmailAsync( + // organizer.Email, + // "Votre compte Hello!", + // new UserCreationModel + // { + // Title = "Création de votre compte Hello!", + // Salutation = $"Bonjour {organizer.Organization},", + // AccountCreatedText = "Votre compte Hello a été créé!", + // TemporaryPasswordHeader = "Votre mot de passe temporaire est: ", + // TemporaryPassword = strongPassword, + // LoginButtonText = "Se connecter", + // ButtonLink = new Uri($"{frontBaseUrl}/fr/login") + // }, + // emails.EmailsUtils.UserCreationTemplate + // ); + + // return Ok(new Response { Data = created }); + //} /// /// Get all users with pagination and search term @@ -131,7 +130,7 @@ public IActionResult GetUsers(string? search, OrganizerAccountActiveFilter filte /// pass a reason for the toggle active change, will be send by email /// [HttpPatch("{organizerId}/toggle")] - public async Task ToggleOrganizer(Guid organizerId, [FromQuery] string? reason) + public async Task ToggleOrganizer(string organizerId, [FromQuery] string? reason) { var success = userService.ToggleUserActiveState(organizerId); var organizer = userService.GetUser(organizerId); diff --git a/core/Controllers/TestController.cs b/core/Controllers/TestController.cs index 4c6f4a8..b4191b7 100644 --- a/core/Controllers/TestController.cs +++ b/core/Controllers/TestController.cs @@ -1,4 +1,7 @@ -using api.core.Data.Requests; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; @@ -14,14 +17,71 @@ namespace api.core.controllers; public class TestController(IConfiguration configuration) : ControllerBase { - [HttpPost("login")] - public async Task Login([FromBody] LoginRequestDTO req, CancellationToken ct) + [HttpGet] + public IActionResult Login() { - var projectId = configuration.GetValue("SUPABASE_PROJECT_ID"); - var anonKey = configuration.GetValue("SUPABASE_ANON_KEY"); + var redirectionURL = Environment.GetEnvironmentVariable("OPENID_BASE_URL") + "authorize/?"; + Dictionary queryParameters = new() + { + ["client_id"] = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID"), + ["response_type"] = "code", + ["redirect_uri"] = "https://localhost:8081/callback/code", + ["scope"] = "email,profile,openid", + ["state"] = "1234" + }; + + return Redirect(redirectionURL + string.Join('&', queryParameters.Select(qp => qp.Key + '=' + qp.Value))); + } + + [HttpGet] + [Route("/callback/code")] + public async Task Reception([FromQuery] string code, [FromQuery] string? state) + { + using HttpClient client = new(); + string claimUrl = Environment.GetEnvironmentVariable("OPENID_BASE_URL") + "token/"; + + Dictionary body = new() + { + ["grant_type"] = "authorization_code", + ["redirect_uri"] = Request.Scheme + "://" + Request.Host.Value, + ["code"] = code + }; + + string clientId = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID"); + string clientSecret = Environment.GetEnvironmentVariable("OPENID_CLIENT_SECRET"); + + using HttpRequestMessage request = new(HttpMethod.Post, claimUrl); + + request.Content = new FormUrlEncodedContent(body); + request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"))); + + HttpResponseMessage response = await client.SendAsync(request); - var client = new Supabase.Client($"https://{projectId}.supabase.co", anonKey); - var response = await client.Auth.SignInWithPassword(req.Email, req.Password); - return Ok(response); + if (! response.IsSuccessStatusCode) + { + return BadRequest(); + } + + string contenu = await response.Content.ReadAsStringAsync(); + + JsonSerializerOptions settings = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + TokenResponse token = JsonSerializer.Deserialize(contenu, settings)!; + + return Ok(token); } +} + +[JsonSerializable(typeof(TokenResponse))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] +public record class TokenResponse +{ + public string AccessToken { get; set; } + public string TokenType { get; set; } + public string Scope { get; set; } + public string IdToken { get; set; } + public int ExpiresIn { get; set; } } \ No newline at end of file diff --git a/core/Data/Entities/ActivityArea.cs b/core/Data/Entities/ActivityArea.cs index 8bd6245..8bdaf7b 100644 --- a/core/Data/Entities/ActivityArea.cs +++ b/core/Data/Entities/ActivityArea.cs @@ -12,9 +12,15 @@ public partial class ActivityArea : BaseEntity public string NameEn { get; set; } = null!; - [InverseProperty("ActivityArea")] - public virtual ICollection Organizers { get; set; } = new List(); + /// + /// Groups all Users, no matter their role + /// + [InverseProperty(nameof(User.ActivityArea))] + public virtual ICollection Users { get; set; } = new List(); - [InverseProperty("ActivityArea")] - public virtual ICollection Moderators { get; set; } = new List(); + [NotMapped] + public ICollection Organizers => Users.Where(u => u.Role.HasFlag(UserRole.Organizer)).ToList(); + + [NotMapped] + public ICollection Moderators => Users.Where(u => u.Role.HasFlag(UserRole.Moderator)).ToList(); } diff --git a/core/Data/Entities/Moderator.cs b/core/Data/Entities/Moderator.cs deleted file mode 100644 index 9854801..0000000 --- a/core/Data/Entities/Moderator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; - -using api.core.Data.Entities; - -namespace api.core.data.entities; - -[Table("Moderator")] -public partial class Moderator : User -{ - [InverseProperty("Moderator")] - public virtual ICollection Publications { get; set; } = new List(); - - [ForeignKey("ActivityAreaId")] - [InverseProperty("Moderators")] - public virtual ActivityArea? ActivityArea { get; set; } -} diff --git a/core/Data/Entities/Organizer.cs b/core/Data/Entities/Organizer.cs deleted file mode 100644 index c4ac9b8..0000000 --- a/core/Data/Entities/Organizer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; - -using api.core.Data.Entities; - -namespace api.core.data.entities; - -[Table("Organizer")] -public partial class Organizer : User -{ - public string Organization { get; set; } = null!; - - public string ProfileDescription { get; set; } = null!; - - public bool IsActive { get; set; } - - public bool HasLoggedIn { get; set; } - - public string? FacebookLink { get; set; } - - public string? InstagramLink { get; set; } - - public string? TikTokLink { get; set; } - - public string? XLink { get; set; } - - public string? DiscordLink { get; set; } - - public string? LinkedInLink { get; set; } - - public string? RedditLink { get; set; } - - public string? WebSiteLink { get; set; } - - [InverseProperty("Organizer")] - public virtual ICollection Publications { get; set; } = new List(); - - [ForeignKey("ActivityAreaId")] - [InverseProperty("Organizers")] - public virtual ActivityArea? ActivityArea { get; set; } - - [InverseProperty(nameof(Subscription.Organizer))] - public virtual ICollection Subscriptions { get; set; } = new List(); -} diff --git a/core/Data/Entities/Publication.cs b/core/Data/Entities/Publication.cs index 33c13fd..7f9fb79 100644 --- a/core/Data/Entities/Publication.cs +++ b/core/Data/Entities/Publication.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using api.core.Data.Entities; using api.core.Data.Enums; using Microsoft.EntityFrameworkCore; @@ -36,9 +37,9 @@ public partial class Publication public bool HasBeenReported { get; set; } = false; - public Guid? ModeratorId { get; set; } + public string? ModeratorId { get; set; } - public Guid OrganizerId { get; set; } + public string OrganizerId { get; set; } = null!; public DateTime CreatedAt { get; set; } @@ -50,12 +51,10 @@ public partial class Publication public virtual Event? Event { get; set; } [ForeignKey("ModeratorId")] - [InverseProperty("Publications")] - public virtual Moderator? Moderator { get; set; } + public virtual User? Moderator { get; set; } [ForeignKey("OrganizerId")] - [InverseProperty("Publications")] - public virtual Organizer Organizer { get; set; } = null!; + public virtual User Organizer { get; set; } = null!; [InverseProperty("Publication")] public virtual ICollection Reports { get; set; } = new List(); diff --git a/core/Data/Entities/Subscription.cs b/core/Data/Entities/Subscription.cs index 93a804b..cce634e 100644 --- a/core/Data/Entities/Subscription.cs +++ b/core/Data/Entities/Subscription.cs @@ -9,14 +9,14 @@ namespace api.core.data.entities; public partial class Subscription : BaseEntity { public string Email { get; set; } = null!; - - public Guid OrganizerId { get; set; } + + public string OrganizerId { get; set; } = null!; public string SubscriptionToken { get; set; } = null!; [ForeignKey(nameof(OrganizerId))] - [InverseProperty(nameof(entities.Organizer.Subscriptions))] - public virtual Organizer Organizer { get; set; } = null!; + [InverseProperty(nameof(User.Subscriptions))] + public virtual User Organizer { get; set; } = null!; [InverseProperty(nameof(Notification.Subscription))] public virtual ICollection Notifications { get; set; } = new List(); diff --git a/core/Data/Entities/User.cs b/core/Data/Entities/User.cs index bfc6fb5..3886b7f 100644 --- a/core/Data/Entities/User.cs +++ b/core/Data/Entities/User.cs @@ -1,8 +1,73 @@ -namespace api.core.Data.Entities; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; -public class User : BaseEntity +using api.core.data.entities; + +namespace api.core.Data.Entities; + +[Table(nameof(User))] +public class User { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public string Id { get; set; } = null!; + public string Email { get; set; } = null!; public Guid? ActivityAreaId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + + public DateTime? DeletedAt { get; set; } + + public UserRole Role { get; set; } + + // Propriétés de Organizer + public string Organization { get; set; } = null!; + + public string ProfileDescription { get; set; } = null!; + + public bool IsActive { get; set; } + + public bool HasLoggedIn { get; set; } + + public string? FacebookLink { get; set; } + + public string? InstagramLink { get; set; } + + public string? TikTokLink { get; set; } + + public string? XLink { get; set; } + + public string? DiscordLink { get; set; } + + public string? LinkedInLink { get; set; } + + public string? RedditLink { get; set; } + + public string? WebSiteLink { get; set; } + + [ForeignKey(nameof(ActivityAreaId))] + [InverseProperty(nameof(ActivityArea.Users))] + public virtual ActivityArea? ActivityArea { get; set; } + + [InverseProperty(nameof(Subscription.Organizer))] + public virtual ICollection Subscriptions { get; set; } = new List(); + + // Il ne semble pas nécessaire de mapper les publications dans le User pour le moment + //public virtual ICollection Publications { get; set; } = new List(); } + +/// +/// Specifies the roles that a user can have within the system. +/// +/// This enumeration supports bitwise combination of its member values. A User therefore can have multiple roles. +[Flags] +public enum UserRole +{ + Admin = 0b10000000, + Moderator = 0b01000000, + Organizer = 0b00100000 +} \ No newline at end of file diff --git a/core/Data/EventManagementContext.cs b/core/Data/EventManagementContext.cs index 2993645..494776a 100644 --- a/core/Data/EventManagementContext.cs +++ b/core/Data/EventManagementContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using api.core.data.entities; +using api.core.Data.Entities; using Microsoft.EntityFrameworkCore; @@ -20,9 +21,11 @@ public EventManagementContext(DbContextOptions options) public virtual DbSet Events { get; set; } - public virtual DbSet Moderators { get; set; } + //public virtual DbSet Moderators { get; set; } - public virtual DbSet Organizers { get; set; } + //public virtual DbSet Organizers { get; set; } + + public virtual DbSet Users { get; set; } public virtual DbSet Publications { get; set; } @@ -43,16 +46,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Id).ValueGeneratedNever(); }); - modelBuilder.Entity(entity => - { - entity.Property(e => e.Id).HasDefaultValueSql("gen_random_uuid()"); - entity.Property(e => e.CreatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); - entity.Property(e => e.UpdatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); - }); - - modelBuilder.Entity(entity => + modelBuilder.Entity(entity => { - entity.Property(e => e.Id).HasDefaultValueSql("gen_random_uuid()"); entity.Property(e => e.CreatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); entity.Property(e => e.UpdatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); }); @@ -63,10 +58,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.CreatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); entity.Property(e => e.UpdatedAt).HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); - entity.HasOne(d => d.Moderator).WithMany(p => p.Publications).HasConstraintName("Publication_ModeratorId_fkey"); - - entity.HasOne(d => d.Organizer).WithMany(p => p.Publications).HasConstraintName("Publication_OrganizerId_fkey"); - entity.HasMany(d => d.Tags).WithMany(p => p.Publications) .UsingEntity>( "PublicationTag", diff --git a/core/Data/Requests/LoginRequestDTO.cs b/core/Data/Requests/LoginRequestDTO.cs deleted file mode 100644 index 5c3ca48..0000000 --- a/core/Data/Requests/LoginRequestDTO.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace api.core.Data.Requests; - -public class LoginRequestDTO -{ - public required string Email { get; set; } - public required string Password { get; set; } -} diff --git a/core/Data/Requests/ModeratorRequestDTO.cs b/core/Data/Requests/ModeratorRequestDTO.cs index 5c376ca..4c69bd3 100644 --- a/core/Data/Requests/ModeratorRequestDTO.cs +++ b/core/Data/Requests/ModeratorRequestDTO.cs @@ -5,7 +5,7 @@ public class ModeratorCreateRequestDTO /// /// This id when passed needs to be already created in supabase. /// - public required Guid Id { get; set; } + public required string Id { get; set; } /// /// A email that will be bound to the moderator, make sure that this email match the diff --git a/core/Data/Requests/SubscriptionRequestDTO.cs b/core/Data/Requests/SubscriptionRequestDTO.cs index 17935b3..6c46216 100644 --- a/core/Data/Requests/SubscriptionRequestDTO.cs +++ b/core/Data/Requests/SubscriptionRequestDTO.cs @@ -4,7 +4,7 @@ public class SubscribeRequestDTO { public required string Email { get; set; } - public required Guid OrganizerId { get; set; } + public required string OrganizerId { get; set; } } diff --git a/core/Data/Requests/UserRequestDTO.cs b/core/Data/Requests/UserRequestDTO.cs index 7f4e988..de0f2b2 100644 --- a/core/Data/Requests/UserRequestDTO.cs +++ b/core/Data/Requests/UserRequestDTO.cs @@ -2,6 +2,8 @@ public class UserCreateDTO { + public required string Id { get; set; } = null!; + public required string Email { get; set; } public string? Organization { get; set; } = null!; @@ -11,8 +13,6 @@ public class UserCreateDTO public class UserUpdateDTO : UserCreateDTO { - public Guid Id { get; set; } - public bool? HasLoggedIn { get; set; } public string? ProfileDescription { get; set; } = null!; diff --git a/core/Data/Responses/ModeratorRequestDTO.cs b/core/Data/Responses/ModeratorRequestDTO.cs index 0e1b09f..9d937ed 100644 --- a/core/Data/Responses/ModeratorRequestDTO.cs +++ b/core/Data/Responses/ModeratorRequestDTO.cs @@ -2,7 +2,7 @@ public class ModeratorResponseDTO { - public required Guid Id { get; set; } + public required string Id { get; set; } public required string Email { get; set; } public DateTime? CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } diff --git a/core/Data/Responses/UserResponseDTO.cs b/core/Data/Responses/UserResponseDTO.cs index 41f4eff..254a49a 100644 --- a/core/Data/Responses/UserResponseDTO.cs +++ b/core/Data/Responses/UserResponseDTO.cs @@ -1,10 +1,11 @@ using api.core.data.entities; +using api.core.Data.Entities; namespace api.core.Data.Responses; public class UserResponseDTO { - public Guid Id { get; set; } + public string Id { get; set; } public string Name { get; set; } = null!; @@ -46,44 +47,79 @@ public class UserResponseDTO public DateTime UpdatedAt { get; set; } - public static UserResponseDTO Map(Organizer organizer) + //public static UserResponseDTO Map(Organizer organizer) + //{ + // return new UserResponseDTO + // { + // Id = organizer.Id, + // Email = organizer.Email, + // Type = "Organizer", + // Organization = organizer.Organization, + // ActivityArea = organizer.ActivityArea != null ? + // ActivityAreaResponseDTO.Map(organizer.ActivityArea) : + // null, + // IsActive = organizer.IsActive, + // HasLoggedIn = organizer.HasLoggedIn, + // ProfileDescription = organizer.ProfileDescription, + // FacebookLink = organizer.FacebookLink, + // InstagramLink = organizer.InstagramLink, + // TikTokLink = organizer.TikTokLink, + // XLink = organizer.XLink, + // DiscordLink = organizer.DiscordLink, + // LinkedInLink = organizer.LinkedInLink, + // RedditLink = organizer.RedditLink, + // WebSiteLink = organizer.WebSiteLink, + // CreatedAt = organizer.CreatedAt, + // UpdatedAt = organizer.UpdatedAt + // }; + //} + + //public static UserResponseDTO Map(Moderator moderator) + //{ + // return new UserResponseDTO + // { + // Id = moderator.Id, + // Email = moderator.Email, + // Type = "Moderator", + // IsActive = true, + // HasLoggedIn = true, + // CreatedAt = moderator.CreatedAt, + // UpdatedAt = moderator.UpdatedAt + // }; + //} + + public static UserResponseDTO Map(User user) { - return new UserResponseDTO + List roles = new List(); + foreach (UserRole role in Enum.GetValues(typeof(UserRole))) { - Id = organizer.Id, - Email = organizer.Email, - Type = "Organizer", - Organization = organizer.Organization, - ActivityArea = organizer.ActivityArea != null ? - ActivityAreaResponseDTO.Map(organizer.ActivityArea) : - null, - IsActive = organizer.IsActive, - HasLoggedIn = organizer.HasLoggedIn, - ProfileDescription = organizer.ProfileDescription, - FacebookLink = organizer.FacebookLink, - InstagramLink = organizer.InstagramLink, - TikTokLink = organizer.TikTokLink, - XLink = organizer.XLink, - DiscordLink = organizer.DiscordLink, - LinkedInLink = organizer.LinkedInLink, - RedditLink = organizer.RedditLink, - WebSiteLink = organizer.WebSiteLink, - CreatedAt = organizer.CreatedAt, - UpdatedAt = organizer.UpdatedAt - }; - } + if (user.Role.HasFlag(role)) + { + roles.Add(role); + } + } - public static UserResponseDTO Map(Moderator moderator) - { return new UserResponseDTO { - Id = moderator.Id, - Email = moderator.Email, - Type = "Moderator", - IsActive = true, - HasLoggedIn = true, - CreatedAt = moderator.CreatedAt, - UpdatedAt = moderator.UpdatedAt + Id = user.Id, + Email = user.Email, + Type = string.Join(',', roles.Select(r => r.ToString())), + IsActive = user.IsActive, + HasLoggedIn = user.HasLoggedIn, + ProfileDescription = user.ProfileDescription, + FacebookLink = user.FacebookLink, + InstagramLink = user.InstagramLink, + TikTokLink = user.TikTokLink, + XLink = user.XLink, + DiscordLink = user.DiscordLink, + LinkedInLink = user.LinkedInLink, + RedditLink = user.RedditLink, + WebSiteLink = user.WebSiteLink, + Organization = user.Organization, + ActivityArea = user.ActivityArea != null ? + ActivityAreaResponseDTO.Map(user.ActivityArea) : null, + CreatedAt = user.CreatedAt, + UpdatedAt = user.UpdatedAt }; } } diff --git a/core/Extensions/DependencyInjectionExtension.cs b/core/Extensions/DependencyInjectionExtension.cs index 8e77108..a3e52ed 100644 --- a/core/Extensions/DependencyInjectionExtension.cs +++ b/core/Extensions/DependencyInjectionExtension.cs @@ -1,6 +1,7 @@ using api.core.Misc; using api.core.repositories; using api.core.repositories.abstractions; +using api.core.Repositories.Abstractions; using api.core.services.abstractions; using api.core.Services; using api.core.Services.Abstractions; @@ -9,24 +10,19 @@ using api.files.Services; using api.files.Services.Abstractions; -using Supabase; - namespace api.core.Extensions; public static class DependencyInjectionExtension { public static IServiceCollection AddDependencyInjection(this IServiceCollection services) { - AddSupabase(services); - // Middlewares services.AddTransient(); // Repositories - services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -38,7 +34,6 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -47,13 +42,10 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection services.AddTransient(); services.AddTransient(); + // Utils + services.AddTransient(); + return services; } - private static void AddSupabase(IServiceCollection services) - { - var url = $"https://{Environment.GetEnvironmentVariable("SUPABASE_PROJECT_ID")}.supabase.co"; - var key = Environment.GetEnvironmentVariable("SUPABASE_ANON_KEY"); - services.AddSingleton(provider => new Client(url, key)); - } } \ No newline at end of file diff --git a/core/Migrations/20251230035918_UserMerge.Designer.cs b/core/Migrations/20251230035918_UserMerge.Designer.cs new file mode 100644 index 0000000..664c69b --- /dev/null +++ b/core/Migrations/20251230035918_UserMerge.Designer.cs @@ -0,0 +1,526 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.core.data; + +#nullable disable + +namespace api.core.Migrations +{ + [DbContext(typeof(EventManagementContext))] + [Migration("20251230035918_UserMerge")] + partial class UserMerge + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PublicationTag", b => + { + b.Property("PublicationsId") + .HasColumnType("uuid"); + + b.Property("TagsId") + .HasColumnType("uuid"); + + b.HasKey("PublicationsId", "TagsId"); + + b.HasIndex(new[] { "TagsId" }, "IX_PublicationTag_TagsId"); + + b.ToTable("PublicationTag", (string)null); + }); + + modelBuilder.Entity("TagTag", b => + { + b.Property("ChildrenTagsId") + .HasColumnType("uuid"); + + b.Property("ParentTagsId") + .HasColumnType("uuid"); + + b.HasKey("ChildrenTagsId", "ParentTagsId"); + + b.ToTable("TagTag"); + }); + + modelBuilder.Entity("TagsHierarchy", b => + { + b.Property("ChildrenTagsId") + .HasColumnType("uuid"); + + b.Property("ParentTagsId") + .HasColumnType("uuid"); + + b.HasKey("ChildrenTagsId", "ParentTagsId"); + + b.HasIndex(new[] { "ParentTagsId" }, "IX_TagsHierarchy_ParentTagsId"); + + b.ToTable("TagsHierarchy", (string)null); + }); + + modelBuilder.Entity("api.core.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ActivityAreaId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscordLink") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacebookLink") + .HasColumnType("text"); + + b.Property("HasLoggedIn") + .HasColumnType("boolean"); + + b.Property("InstagramLink") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LinkedInLink") + .HasColumnType("text"); + + b.Property("Organization") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfileDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("RedditLink") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("TikTokLink") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("WebSiteLink") + .HasColumnType("text"); + + b.Property("XLink") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityAreaId"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("api.core.data.entities.ActivityArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NameEn") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameFr") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ActivityArea"); + }); + + modelBuilder.Entity("api.core.data.entities.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EventEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventStartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("api.core.data.entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsSent") + .HasColumnType("boolean"); + + b.Property("PublicationId") + .HasColumnType("uuid"); + + b.Property("SubscriptionId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PublicationId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HasBeenReported") + .HasColumnType("boolean"); + + b.Property("ImageAltText") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("OrganizerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReportCount") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ModeratorId" }, "IX_Publication_ModeratorId"); + + b.HasIndex(new[] { "OrganizerId" }, "IX_Publication_OrganizerId"); + + b.ToTable("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PublicationId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "PublicationId" }, "IX_Report_PublicationId"); + + b.ToTable("Report"); + }); + + modelBuilder.Entity("api.core.data.entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubscriptionToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizerId"); + + b.HasIndex("Email", "OrganizerId") + .IsUnique(); + + b.ToTable("Subscription"); + }); + + modelBuilder.Entity("api.core.data.entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriorityValue") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("PublicationTag", b => + { + b.HasOne("api.core.data.entities.Publication", null) + .WithMany() + .HasForeignKey("PublicationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TagsHierarchy", b => + { + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("ChildrenTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("ParentTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("api.core.Data.Entities.User", b => + { + b.HasOne("api.core.data.entities.ActivityArea", "ActivityArea") + .WithMany("Users") + .HasForeignKey("ActivityAreaId"); + + b.Navigation("ActivityArea"); + }); + + modelBuilder.Entity("api.core.data.entities.Event", b => + { + b.HasOne("api.core.data.entities.Publication", "Publication") + .WithOne("Event") + .HasForeignKey("api.core.data.entities.Event", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Notification", b => + { + b.HasOne("api.core.data.entities.Publication", "Publication") + .WithMany("Notifications") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.core.data.entities.Subscription", "Subscription") + .WithMany("Notifications") + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.HasOne("api.core.Data.Entities.User", "Moderator") + .WithMany() + .HasForeignKey("ModeratorId"); + + b.HasOne("api.core.Data.Entities.User", "Organizer") + .WithMany() + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Moderator"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("api.core.data.entities.Report", b => + { + b.HasOne("api.core.data.entities.Publication", "Publication") + .WithMany("Reports") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("Report_PublicationId_fkey"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Subscription", b => + { + b.HasOne("api.core.Data.Entities.User", "Organizer") + .WithMany("Subscriptions") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("api.core.Data.Entities.User", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("api.core.data.entities.ActivityArea", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.Navigation("Event"); + + b.Navigation("Notifications"); + + b.Navigation("Reports"); + }); + + modelBuilder.Entity("api.core.data.entities.Subscription", b => + { + b.Navigation("Notifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/core/Migrations/20251230035918_UserMerge.cs b/core/Migrations/20251230035918_UserMerge.cs new file mode 100644 index 0000000..fb24f62 --- /dev/null +++ b/core/Migrations/20251230035918_UserMerge.cs @@ -0,0 +1,251 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api.core.Migrations +{ + /// + public partial class UserMerge : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "Publication_ModeratorId_fkey", + table: "Publication"); + + migrationBuilder.DropForeignKey( + name: "Publication_OrganizerId_fkey", + table: "Publication"); + + migrationBuilder.DropForeignKey( + name: "FK_Subscription_Organizer_OrganizerId", + table: "Subscription"); + + migrationBuilder.DropTable( + name: "Moderator"); + + migrationBuilder.DropTable( + name: "Organizer"); + + migrationBuilder.AlterColumn( + name: "OrganizerId", + table: "Subscription", + type: "text", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OrganizerId", + table: "Publication", + type: "text", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ModeratorId", + table: "Publication", + type: "text", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "User", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + ActivityAreaId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Role = table.Column(type: "integer", nullable: false), + Organization = table.Column(type: "text", nullable: false), + ProfileDescription = table.Column(type: "text", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + HasLoggedIn = table.Column(type: "boolean", nullable: false), + FacebookLink = table.Column(type: "text", nullable: true), + InstagramLink = table.Column(type: "text", nullable: true), + TikTokLink = table.Column(type: "text", nullable: true), + XLink = table.Column(type: "text", nullable: true), + DiscordLink = table.Column(type: "text", nullable: true), + LinkedInLink = table.Column(type: "text", nullable: true), + RedditLink = table.Column(type: "text", nullable: true), + WebSiteLink = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_User", x => x.Id); + table.ForeignKey( + name: "FK_User_ActivityArea_ActivityAreaId", + column: x => x.ActivityAreaId, + principalTable: "ActivityArea", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_User_ActivityAreaId", + table: "User", + column: "ActivityAreaId"); + + migrationBuilder.AddForeignKey( + name: "FK_Publication_User_ModeratorId", + table: "Publication", + column: "ModeratorId", + principalTable: "User", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Publication_User_OrganizerId", + table: "Publication", + column: "OrganizerId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Subscription_User_OrganizerId", + table: "Subscription", + column: "OrganizerId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Publication_User_ModeratorId", + table: "Publication"); + + migrationBuilder.DropForeignKey( + name: "FK_Publication_User_OrganizerId", + table: "Publication"); + + migrationBuilder.DropForeignKey( + name: "FK_Subscription_User_OrganizerId", + table: "Subscription"); + + migrationBuilder.DropTable( + name: "User"); + + migrationBuilder.AlterColumn( + name: "OrganizerId", + table: "Subscription", + type: "uuid", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizerId", + table: "Publication", + type: "uuid", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "ModeratorId", + table: "Publication", + type: "uuid", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "Moderator", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + ActivityAreaId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Email = table.Column(type: "text", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)") + }, + constraints: table => + { + table.PrimaryKey("PK_Moderator", x => x.Id); + table.ForeignKey( + name: "FK_Moderator_ActivityArea_ActivityAreaId", + column: x => x.ActivityAreaId, + principalTable: "ActivityArea", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Organizer", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + ActivityAreaId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + DiscordLink = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + FacebookLink = table.Column(type: "text", nullable: true), + HasLoggedIn = table.Column(type: "boolean", nullable: false), + InstagramLink = table.Column(type: "text", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + LinkedInLink = table.Column(type: "text", nullable: true), + Organization = table.Column(type: "text", nullable: false), + ProfileDescription = table.Column(type: "text", nullable: false), + RedditLink = table.Column(type: "text", nullable: true), + TikTokLink = table.Column(type: "text", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "(now() AT TIME ZONE 'utc'::text)"), + WebSiteLink = table.Column(type: "text", nullable: true), + XLink = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Organizer", x => x.Id); + table.ForeignKey( + name: "FK_Organizer_ActivityArea_ActivityAreaId", + column: x => x.ActivityAreaId, + principalTable: "ActivityArea", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Moderator_ActivityAreaId", + table: "Moderator", + column: "ActivityAreaId"); + + migrationBuilder.CreateIndex( + name: "IX_Organizer_ActivityAreaId", + table: "Organizer", + column: "ActivityAreaId"); + + migrationBuilder.AddForeignKey( + name: "Publication_ModeratorId_fkey", + table: "Publication", + column: "ModeratorId", + principalTable: "Moderator", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "Publication_OrganizerId_fkey", + table: "Publication", + column: "OrganizerId", + principalTable: "Organizer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Subscription_Organizer_OrganizerId", + table: "Subscription", + column: "OrganizerId", + principalTable: "Organizer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/core/Migrations/EventManagementContextModelSnapshot.cs b/core/Migrations/EventManagementContextModelSnapshot.cs index 6d38530..58e5b44 100644 --- a/core/Migrations/EventManagementContextModelSnapshot.cs +++ b/core/Migrations/EventManagementContextModelSnapshot.cs @@ -65,85 +65,81 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("TagsHierarchy", (string)null); }); - modelBuilder.Entity("api.core.data.entities.ActivityArea", b => + modelBuilder.Entity("api.core.Data.Entities.User", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ActivityAreaId") .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); - b.Property("NameEn") - .IsRequired() + b.Property("DiscordLink") .HasColumnType("text"); - b.Property("NameFr") + b.Property("Email") .IsRequired() .HasColumnType("text"); - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("FacebookLink") + .HasColumnType("text"); - b.HasKey("Id"); + b.Property("HasLoggedIn") + .HasColumnType("boolean"); - b.ToTable("ActivityArea"); - }); + b.Property("InstagramLink") + .HasColumnType("text"); - modelBuilder.Entity("api.core.data.entities.Event", b => - { - b.Property("Id") - .HasColumnType("uuid"); + b.Property("IsActive") + .HasColumnType("boolean"); - b.Property("EventEndDate") - .HasColumnType("timestamp with time zone"); + b.Property("LinkedInLink") + .HasColumnType("text"); - b.Property("EventStartDate") - .HasColumnType("timestamp with time zone"); + b.Property("Organization") + .IsRequired() + .HasColumnType("text"); - b.HasKey("Id"); + b.Property("ProfileDescription") + .IsRequired() + .HasColumnType("text"); - b.ToTable("Event"); - }); + b.Property("RedditLink") + .HasColumnType("text"); - modelBuilder.Entity("api.core.data.entities.Moderator", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + b.Property("Role") + .HasColumnType("integer"); - b.Property("ActivityAreaId") - .HasColumnType("uuid"); + b.Property("TikTokLink") + .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() + b.Property("WebSiteLink") .HasColumnType("text"); - b.Property("UpdatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + b.Property("XLink") + .HasColumnType("text"); b.HasKey("Id"); b.HasIndex("ActivityAreaId"); - b.ToTable("Moderator"); + b.ToTable("User"); }); - modelBuilder.Entity("api.core.data.entities.Notification", b => + modelBuilder.Entity("api.core.data.entities.ActivityArea", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -155,97 +151,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); - b.Property("IsSent") - .HasColumnType("boolean"); - - b.Property("PublicationId") - .HasColumnType("uuid"); + b.Property("NameEn") + .IsRequired() + .HasColumnType("text"); - b.Property("SubscriptionId") - .HasColumnType("uuid"); + b.Property("NameFr") + .IsRequired() + .HasColumnType("text"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); - b.HasIndex("PublicationId"); - - b.HasIndex("SubscriptionId"); - - b.ToTable("Notification"); + b.ToTable("ActivityArea"); }); - modelBuilder.Entity("api.core.data.entities.Organizer", b => + modelBuilder.Entity("api.core.data.entities.Event", b => { b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); - - b.Property("ActivityAreaId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + b.Property("EventEndDate") + .HasColumnType("timestamp with time zone"); - b.Property("DeletedAt") + b.Property("EventStartDate") .HasColumnType("timestamp with time zone"); - b.Property("DiscordLink") - .HasColumnType("text"); + b.HasKey("Id"); - b.Property("Email") - .IsRequired() - .HasColumnType("text"); + b.ToTable("Event"); + }); - b.Property("FacebookLink") - .HasColumnType("text"); + modelBuilder.Entity("api.core.data.entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); - b.Property("HasLoggedIn") - .HasColumnType("boolean"); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); - b.Property("InstagramLink") - .HasColumnType("text"); + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); - b.Property("IsActive") + b.Property("IsSent") .HasColumnType("boolean"); - b.Property("LinkedInLink") - .HasColumnType("text"); - - b.Property("Organization") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfileDescription") - .IsRequired() - .HasColumnType("text"); - - b.Property("RedditLink") - .HasColumnType("text"); + b.Property("PublicationId") + .HasColumnType("uuid"); - b.Property("TikTokLink") - .HasColumnType("text"); + b.Property("SubscriptionId") + .HasColumnType("uuid"); b.Property("UpdatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); - - b.Property("WebSiteLink") - .HasColumnType("text"); - - b.Property("XLink") - .HasColumnType("text"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); - b.HasIndex("ActivityAreaId"); + b.HasIndex("PublicationId"); - b.ToTable("Organizer"); + b.HasIndex("SubscriptionId"); + + b.ToTable("Notification"); }); modelBuilder.Entity("api.core.data.entities.Publication", b => @@ -275,11 +243,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImageUrl") .HasColumnType("text"); - b.Property("ModeratorId") - .HasColumnType("uuid"); + b.Property("ModeratorId") + .HasColumnType("text"); - b.Property("OrganizerId") - .HasColumnType("uuid"); + b.Property("OrganizerId") + .IsRequired() + .HasColumnType("text"); b.Property("PublicationDate") .HasColumnType("timestamp with time zone"); @@ -363,8 +332,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("OrganizerId") - .HasColumnType("uuid"); + b.Property("OrganizerId") + .IsRequired() + .HasColumnType("text"); b.Property("SubscriptionToken") .IsRequired() @@ -445,6 +415,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("api.core.Data.Entities.User", b => + { + b.HasOne("api.core.data.entities.ActivityArea", "ActivityArea") + .WithMany("Users") + .HasForeignKey("ActivityAreaId"); + + b.Navigation("ActivityArea"); + }); + modelBuilder.Entity("api.core.data.entities.Event", b => { b.HasOne("api.core.data.entities.Publication", "Publication") @@ -456,15 +435,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Publication"); }); - modelBuilder.Entity("api.core.data.entities.Moderator", b => - { - b.HasOne("api.core.data.entities.ActivityArea", "ActivityArea") - .WithMany("Moderators") - .HasForeignKey("ActivityAreaId"); - - b.Navigation("ActivityArea"); - }); - modelBuilder.Entity("api.core.data.entities.Notification", b => { b.HasOne("api.core.data.entities.Publication", "Publication") @@ -484,28 +454,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Subscription"); }); - modelBuilder.Entity("api.core.data.entities.Organizer", b => - { - b.HasOne("api.core.data.entities.ActivityArea", "ActivityArea") - .WithMany("Organizers") - .HasForeignKey("ActivityAreaId"); - - b.Navigation("ActivityArea"); - }); - modelBuilder.Entity("api.core.data.entities.Publication", b => { - b.HasOne("api.core.data.entities.Moderator", "Moderator") - .WithMany("Publications") - .HasForeignKey("ModeratorId") - .HasConstraintName("Publication_ModeratorId_fkey"); + b.HasOne("api.core.Data.Entities.User", "Moderator") + .WithMany() + .HasForeignKey("ModeratorId"); - b.HasOne("api.core.data.entities.Organizer", "Organizer") - .WithMany("Publications") + b.HasOne("api.core.Data.Entities.User", "Organizer") + .WithMany() .HasForeignKey("OrganizerId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("Publication_OrganizerId_fkey"); + .IsRequired(); b.Navigation("Moderator"); @@ -526,7 +485,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("api.core.data.entities.Subscription", b => { - b.HasOne("api.core.data.entities.Organizer", "Organizer") + b.HasOne("api.core.Data.Entities.User", "Organizer") .WithMany("Subscriptions") .HasForeignKey("OrganizerId") .OnDelete(DeleteBehavior.Cascade) @@ -535,23 +494,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organizer"); }); - modelBuilder.Entity("api.core.data.entities.ActivityArea", b => + modelBuilder.Entity("api.core.Data.Entities.User", b => { - b.Navigation("Moderators"); - - b.Navigation("Organizers"); - }); - - modelBuilder.Entity("api.core.data.entities.Moderator", b => - { - b.Navigation("Publications"); + b.Navigation("Subscriptions"); }); - modelBuilder.Entity("api.core.data.entities.Organizer", b => + modelBuilder.Entity("api.core.data.entities.ActivityArea", b => { - b.Navigation("Publications"); - - b.Navigation("Subscriptions"); + b.Navigation("Users"); }); modelBuilder.Entity("api.core.data.entities.Publication", b => diff --git a/core/Misc/IJwtUtils.cs b/core/Misc/IJwtUtils.cs new file mode 100644 index 0000000..2b04b93 --- /dev/null +++ b/core/Misc/IJwtUtils.cs @@ -0,0 +1,6 @@ +namespace api.core.Misc; + +public interface IJwtUtils +{ + string GetUserIdFromAuthHeader(string authHeader); +} \ No newline at end of file diff --git a/core/Misc/JwtUtils.cs b/core/Misc/JwtUtils.cs index f042695..b4ebe5c 100644 --- a/core/Misc/JwtUtils.cs +++ b/core/Misc/JwtUtils.cs @@ -2,9 +2,9 @@ namespace api.core.Misc; -public static class JwtUtils +public class JwtUtils : IJwtUtils { - public static Guid GetUserIdFromAuthHeader(string authHeader) + public string GetUserIdFromAuthHeader(string authHeader) { var token = authHeader.Replace("Bearer ", ""); var handler = new JwtSecurityTokenHandler(); @@ -18,14 +18,19 @@ public static Guid GetUserIdFromAuthHeader(string authHeader) var payload = jsonToken.Payload; if (payload.TryGetValue("sub", out var userIdValue)) { - if (Guid.TryParse(userIdValue.ToString(), out Guid userId)) + if (userIdValue == null) { - return userId; - } - else - { - throw new ArgumentException("sub is not in a valid Guid format"); + throw new ArgumentException("sub has not a valid value"); } + return userIdValue.ToString()!; + //if (Guid.TryParse(userIdValue.ToString(), out Guid userId)) + //{ + // return userId; + //} + //else + //{ + // throw new ArgumentException("sub is not in a valid Guid format"); + //} } else { diff --git a/core/Policies/IsModerator.cs b/core/Policies/IsModerator.cs index 430eff9..9dc1784 100644 --- a/core/Policies/IsModerator.cs +++ b/core/Policies/IsModerator.cs @@ -13,10 +13,11 @@ public class IsModeratorHandler(IUserService userService) : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsModeratorRequirement requirement) { + // TODO : Remplacer cette politique autrement var identifierClaim = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - var userId = Guid.Parse(identifierClaim ?? throw new UnauthorizedException()); + //var userId = Guid.Parse(identifierClaim ?? throw new UnauthorizedException()); - var user = userService.GetUser(userId); + var user = userService.GetUser(identifierClaim); if (user != null && user.Type != "Moderator") throw new UnauthorizedException(); else diff --git a/core/Policies/OrganizerIsActive.cs b/core/Policies/OrganizerIsActive.cs index 542c103..39e618b 100644 --- a/core/Policies/OrganizerIsActive.cs +++ b/core/Policies/OrganizerIsActive.cs @@ -12,10 +12,11 @@ public class OrganizerIsActiveHandler(IUserService userService) : AuthorizationH { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OrganizerIsActiveRequirement requirement) { + // TODO : Remplacer cette politique autrement var identifierClaim = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - var userId = Guid.Parse(identifierClaim ?? throw new UnauthorizedException()); + //var userId = Guid.Parse(identifierClaim ?? throw new UnauthorizedException()); - var user = userService.GetUser(userId); + var user = userService.GetUser(identifierClaim); if (user.Type != "Organizer" || !user.IsActive) throw new UnauthorizedException(); else diff --git a/core/Program.cs b/core/Program.cs index 28d5cd4..b8bb114 100644 --- a/core/Program.cs +++ b/core/Program.cs @@ -1,140 +1,201 @@ -using api.core.Extensions; - -using System.Text; - -using api.core.data; - -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; -using api.core.Misc; -using api.emails; -using System.Reflection; - -var builder = WebApplication.CreateBuilder(args); - -// Environments setup -string supabaseSecretKey = null!; -string supabaseProjectId = null!; -string connectionString = null!; -string? redisConnString = null!; - -connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? throw new Exception("CONNECTION_STRING is not set"); - -redisConnString = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING"); - -if (!EF.IsDesignTime) -{ - supabaseSecretKey = Environment.GetEnvironmentVariable("SUPABASE_SECRET_KEY") ?? throw new Exception("SUPABASE_SECRET_KEY is not set"); - supabaseProjectId = Environment.GetEnvironmentVariable("SUPABASE_PROJECT_ID") ?? throw new Exception("SUPABASE_PROJECT_ID is not set"); -} - -builder.Configuration.AddEnvironmentVariables(); - -builder.Services.AddDbContext(opt => opt.UseNpgsql(connectionString)); - -builder.Services.AddAuthentication().AddJwtBearer(o => -{ - o.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(supabaseSecretKey)), - ValidAudiences = ["authenticated"], - ValidIssuer = $"https://{supabaseProjectId}.supabase.co/auth/v1" - }; -}); - -builder.Services.SetupScheduler(); - -if (string.IsNullOrEmpty(redisConnString)) -{ - builder.Services.AddStackExchangeRedisOutputCache(options => - { - options.Configuration = redisConnString; - }); -} - -builder.Services.AddOutputCache(options => -{ - options.AddBasePolicy(builder => - builder.Cache()); -}); - -// Errors handling -builder.Services.AddExceptionHandler(); -builder.Services.AddProblemDetails(); - -// Endpoints -builder.Services.AddControllers(); - -builder.Services.AddHealthChecks() - .AddNpgSql(connectionString); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() - { - Name = "Authorization", - Type = SecuritySchemeType.ApiKey, - Scheme = "Bearer", - BearerFormat = "JWT", - In = ParameterLocation.Header, - Description = "JWT Authorization header using the Bearer scheme. " + - "\r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.", - }); - - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - Array.Empty() - } - }); - options.UseInlineDefinitionsForEnums(); - - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - options.IncludeXmlComments(xmlPath); -}); - -builder.Services.AddEmailService(builder.Configuration); - -builder.Services.AddDependencyInjection(); -builder.Services.AddPolicies(); - -builder.Services.AddRateLimiters(); - -var app = builder.Build(); - -await using var scope = app.Services.CreateAsyncScope(); -await using var db = scope.ServiceProvider.GetService(); -await db!.Database.MigrateAsync(); - -app.UseSwagger(); -app.UseSwaggerUI(); - - -app.UseExceptionMiddleware(); - -// app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.MapHealthChecks("/health"); - -await app.Services.AddSchedulerAsync(); - -if (redisConnString != null) - app.UseOutputCache(); - -app.MapControllers(); - -app.Run(); +using System.Reflection; +using System.Text; + +using api.core.data; +using api.core.Extensions; +using api.core.Misc; +using api.emails; + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + + +IdentityModelEventSource.ShowPII = true; + +var builder = WebApplication.CreateBuilder(args); + +// Environments setup +string connectionString = null!; +string? redisConnString = null!; + +connectionString = + Environment.GetEnvironmentVariable("CONNECTION_STRING") + ?? throw new Exception("CONNECTION_STRING is not set"); +redisConnString = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING"); + +builder.Configuration.AddEnvironmentVariables(); + +builder.Services.AddDbContext(opt => opt.UseNpgsql(connectionString)); + + +var key = Encoding.ASCII.GetBytes(Environment.GetEnvironmentVariable("OPENID_CLIENT_SECRET") ?? ""); +builder.Services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + //.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + //{ + // options.RequireHttpsMetadata = false; + // options.SaveToken = true; + // options.Authority = Environment.GetEnvironmentVariable("OPENID_ISSUER"); + + // options.TokenValidationParameters = new TokenValidationParameters + // { + // ValidateIssuer = true, + // ValidateAudience = true, + // ValidIssuer = Environment.GetEnvironmentVariable("OPENID_ISSUER"), + // ValidAudience = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID"), + // NameClaimType = "email" + // }; + //}) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + // The URL of your Identity Provider (e.g., "https://dev-xyz.us.auth0.com/") + // The API will download the public keys from here automatically. + options.Authority = Environment.GetEnvironmentVariable("OPENID_ISSUER"); + + // Who is this token for? (This usually matches the "API Identifier" in your IdP) + options.Audience = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID"); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID") + }; + // Ensure HTTPS is used (should be true in production) + options.RequireHttpsMetadata = true; + }); +//.AddOpenIdConnect(options => +// { +// options.Authority = Environment.GetEnvironmentVariable("OPENID_ISSUER"); +// options.ClientId = Environment.GetEnvironmentVariable("OPENID_CLIENT_ID"); +// //options.ClientSecret = Environment.GetEnvironmentVariable("OPENID_CLIENT_SECRET"); + +// //options.SaveTokens = false; + +// //// TODO : Mettre les scopes requis + +// options.Scope.Add("openid"); +// options.Scope.Add("email"); +// options.Scope.Add("profile"); + +// //options.GetClaimsFromUserInfoEndpoint = true; + +// //options.TokenValidationParameters = new TokenValidationParameters +// //{ +// // NameClaimType = "email" +// //}; + +// //options.Events = new OpenIdConnectEvents +// //{ +// // OnTokenValidated = context => +// // { +// // return Task.CompletedTask; +// // }, +// // OnAuthenticationFailed = context => +// // { +// // return Task.CompletedTask; +// // } +// //}; +// }); + +builder.Services.SetupScheduler(); + +if (string.IsNullOrEmpty(redisConnString)) +{ + builder.Services.AddStackExchangeRedisOutputCache(options => + { + options.Configuration = redisConnString; + }); +} + +builder.Services.AddOutputCache(options => +{ + options.AddBasePolicy(builder => builder.Cache()); +}); + +// Errors handling +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +// Endpoints +builder.Services.AddControllers(); + +builder.Services.AddHealthChecks().AddNpgSql(connectionString); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme. " + + "\r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.", + OpenIdConnectUrl = new Uri(Environment.GetEnvironmentVariable("OPENID_ISSUER")!) + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + options.UseInlineDefinitionsForEnums(); + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); +}); + +builder.Services.AddEmailService(builder.Configuration); + +builder.Services.AddDependencyInjection(); +builder.Services.AddPolicies(); + +builder.Services.AddRateLimiters(); + +var app = builder.Build(); + +await using var scope = app.Services.CreateAsyncScope(); +await using var db = scope.ServiceProvider.GetService(); +await db!.Database.MigrateAsync(); + +app.UseSwagger(); +app.UseSwaggerUI(); + +app.UseExceptionMiddleware(); + +// app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapHealthChecks("/health"); + +await app.Services.AddSchedulerAsync(); + +if (redisConnString != null) + app.UseOutputCache(); + +app.MapControllers(); + +app.Run(); diff --git a/core/Properties/launchSettings.json b/core/Properties/launchSettings.json index f427a98..bbea7fc 100644 --- a/core/Properties/launchSettings.json +++ b/core/Properties/launchSettings.json @@ -1,53 +1,52 @@ -{ - "profiles": { - "http": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5010" - }, - "https": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7272;http://localhost:5010" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Docker": { - "commandName": "Docker", - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", - "environmentVariables": { - "ASPNETCORE_HTTP_PORTS": "8080" - }, - "publishAllPorts": true, - "useSSL": true, - "httpPort": 8080, - "sslPort": 8081, - "DockerfileRunArguments": "--rm --env-file .env" - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:58639", - "sslPort": 44369 - } - } +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5010" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "REDIS_CONNECTION_STRING": "localhost:63790,password=eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81,abortConnect=False,resolvedns=1", + "CONNECTION_STRING": "User ID=postgres;Password=test123;Host=localhost;Port=5432;Database=ps;" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7272;http://localhost:5010" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true, + "httpPort": 8080, + "sslPort": 8081, + "DockerfileRunArguments": "--rm --env-file .env" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58639", + "sslPort": 44369 + } + } } \ No newline at end of file diff --git a/core/Repositories/Abstractions/IModeratorRepository.cs b/core/Repositories/Abstractions/IModeratorRepository.cs deleted file mode 100644 index 5a24b08..0000000 --- a/core/Repositories/Abstractions/IModeratorRepository.cs +++ /dev/null @@ -1,7 +0,0 @@ -using api.core.data.entities; - -namespace api.core.repositories.abstractions; - -public interface IModeratorRepository : IRepository -{ -} diff --git a/core/Repositories/Abstractions/IOrganizerRepository.cs b/core/Repositories/Abstractions/IOrganizerRepository.cs deleted file mode 100644 index a44be31..0000000 --- a/core/Repositories/Abstractions/IOrganizerRepository.cs +++ /dev/null @@ -1,8 +0,0 @@ -using api.core.data.entities; -using api.core.Data.requests; - -namespace api.core.repositories.abstractions; - -public interface IOrganizerRepository : IRepository -{ -} diff --git a/core/Repositories/Abstractions/ISubscriptionRepository.cs b/core/Repositories/Abstractions/ISubscriptionRepository.cs index bdc18e1..023a712 100644 --- a/core/Repositories/Abstractions/ISubscriptionRepository.cs +++ b/core/Repositories/Abstractions/ISubscriptionRepository.cs @@ -4,5 +4,5 @@ namespace api.core.repositories.abstractions; public interface ISubscriptionRepository : IRepository { - public bool IsEntryExists(Guid organizerId, string email); + public bool IsEntryExists(string organizerId, string email); } diff --git a/core/Repositories/Abstractions/ITagRepository.cs b/core/Repositories/Abstractions/ITagRepository.cs index 40b5d37..e751d3f 100644 --- a/core/Repositories/Abstractions/ITagRepository.cs +++ b/core/Repositories/Abstractions/ITagRepository.cs @@ -5,5 +5,5 @@ namespace api.core.repositories.abstractions; public interface ITagRepository : IRepository { - public IEnumerable GetInterestFieldsForOrganizer(Guid organizerId, int take = 3); + public IEnumerable GetInterestFieldsForOrganizer(string organizerId, int take = 3); } diff --git a/core/Repositories/Abstractions/IUserRepository.cs b/core/Repositories/Abstractions/IUserRepository.cs new file mode 100644 index 0000000..546f3c1 --- /dev/null +++ b/core/Repositories/Abstractions/IUserRepository.cs @@ -0,0 +1,14 @@ +using api.core.Data.Entities; + +namespace api.core.Repositories.Abstractions; + +public interface IUserRepository +{ + public User Add(User entity); + public bool Delete(User entity); + public User? Get(string id); + public IQueryable GetAll(); + public bool Update(string id, User entity); + public User? GetOrganizer(string id); + public User? GetModerator(string id); +} diff --git a/core/Repositories/OrganizerRepository.cs b/core/Repositories/OrganizerRepository.cs deleted file mode 100644 index 9daf67a..0000000 --- a/core/Repositories/OrganizerRepository.cs +++ /dev/null @@ -1,59 +0,0 @@ -using api.core.data; -using api.core.data.entities; -using api.core.repositories.abstractions; - -using Microsoft.EntityFrameworkCore; - -namespace api.core.repositories; - -public class OrganizerRepository(EventManagementContext context) : IOrganizerRepository -{ - public Organizer Add(Organizer entity) - { - var inserted = context.Organizers.Add(entity); - - if (inserted.Entity != null) - { - context.SaveChanges(); - return inserted.Entity; - } - throw new Exception($"Unable to create an organizer {entity.Id}"); - } - - public bool Delete(Organizer entity) - { - throw new NotImplementedException(); - } - - public Organizer? Get(Guid id) - { - var entity = context.Organizers - .Include(x => x.ActivityArea) - .FirstOrDefault(x => x.Id == id); - if (entity != null && entity.DeletedAt == null) - { - return entity; - } - return null; - } - - public IQueryable GetAll() - { - return context.Organizers - .Include(x => x.ActivityArea); - } - - public bool Update(Guid id, Organizer entity) - { - var existingEntity = Get(id); - - if (existingEntity != null) - { - context.Entry(existingEntity).CurrentValues.SetValues(entity); - context.SaveChanges(); - return true; - } - - return false; - } -} diff --git a/core/Repositories/SubscriptionRepository.cs b/core/Repositories/SubscriptionRepository.cs index 9c9a319..73c9b01 100644 --- a/core/Repositories/SubscriptionRepository.cs +++ b/core/Repositories/SubscriptionRepository.cs @@ -72,7 +72,7 @@ public bool Update(Guid id, Subscription entity) return false; } - public bool IsEntryExists(Guid organizerId, string email) + public bool IsEntryExists(string organizerId, string email) { return context.Subscriptions .Any(x => x.OrganizerId == organizerId && x.Email == email); diff --git a/core/Repositories/TagRepository.cs b/core/Repositories/TagRepository.cs index 488df81..9b6b6e5 100644 --- a/core/Repositories/TagRepository.cs +++ b/core/Repositories/TagRepository.cs @@ -49,7 +49,7 @@ public IQueryable GetAll() return context.Tags; } - public IEnumerable GetInterestFieldsForOrganizer(Guid organizerId, int take = 3) + public IEnumerable GetInterestFieldsForOrganizer(string organizerId, int take = 3) { return context.Publications .Include(x => x.Tags) diff --git a/core/Repositories/ModeratorRepository.cs b/core/Repositories/UserRepository.cs similarity index 50% rename from core/Repositories/ModeratorRepository.cs rename to core/Repositories/UserRepository.cs index f7f3d72..2b954ba 100644 --- a/core/Repositories/ModeratorRepository.cs +++ b/core/Repositories/UserRepository.cs @@ -1,14 +1,16 @@ using api.core.data; using api.core.data.entities; +using api.core.Data.Entities; using api.core.repositories.abstractions; +using api.core.Repositories.Abstractions; namespace api.core.repositories; -public class ModeratorRepository(EventManagementContext context) : IModeratorRepository +public class UserRepository(EventManagementContext context) : IUserRepository { - public Moderator Add(Moderator entity) + public User Add(User entity) { - var inserted = context.Moderators.Add(entity); + var inserted = context.Users.Add(entity); if (inserted.Entity != null) { @@ -18,14 +20,14 @@ public Moderator Add(Moderator entity) throw new Exception($"Unable to create a Moderator {entity.Id}"); } - public bool Delete(Moderator entity) + public bool Delete(User entity) { throw new NotImplementedException(); } - public Moderator? Get(Guid id) + public User? Get(string id) { - var entity = context.Moderators.Find(id); + var entity = context.Users.Find(id); if (entity != null && entity.DeletedAt == null) { return entity; @@ -33,12 +35,12 @@ public bool Delete(Moderator entity) return null; } - public IQueryable GetAll() + public IQueryable GetAll() { throw new NotImplementedException(); } - public bool Update(Guid id, Moderator entity) + public bool Update(string id, User entity) { var existingEntity = Get(id); @@ -51,4 +53,26 @@ public bool Update(Guid id, Moderator entity) return false; } + + public User? GetOrganizer(string id) + { + var user = Get(id); + if (user == null || !user.Role.HasFlag(UserRole.Organizer)) + { + return null; + } + + return user; + } + + public User? GetModerator(string id) + { + var user = Get(id); + if (user == null || !user.Role.HasFlag(UserRole.Moderator)) + { + return null; + } + + return user; + } } diff --git a/core/Services/Abstractions/IAuthService.cs b/core/Services/Abstractions/IAuthService.cs deleted file mode 100644 index 8de4bda..0000000 --- a/core/Services/Abstractions/IAuthService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api.core.services.abstractions; - -public interface IAuthService -{ - string SignUp(string email, string password); -} diff --git a/core/Services/Abstractions/IDraftEventService.cs b/core/Services/Abstractions/IDraftEventService.cs index 549dda7..33e3e6c 100644 --- a/core/Services/Abstractions/IDraftEventService.cs +++ b/core/Services/Abstractions/IDraftEventService.cs @@ -8,7 +8,7 @@ namespace api.core.services.abstractions; public interface IDraftEventService { - public EventResponseDTO AddDraftEvent(Guid userId, DraftEventRequestDTO request); + public EventResponseDTO AddDraftEvent(string userId, DraftEventRequestDTO request); - public bool UpdateDraftEvent(Guid userId, Guid eventId, DraftEventRequestDTO request); + public bool UpdateDraftEvent(string userId, Guid eventId, DraftEventRequestDTO request); } diff --git a/core/Services/Abstractions/IEventService.cs b/core/Services/Abstractions/IEventService.cs index b24e023..d0ea487 100644 --- a/core/Services/Abstractions/IEventService.cs +++ b/core/Services/Abstractions/IEventService.cs @@ -6,17 +6,17 @@ namespace api.core.services.abstractions; public interface IEventService { - public IEnumerable GetEvents(DateTime? startDate, DateTime? endDate, IEnumerable? activityAreas, IEnumerable? tags, Guid? organizerId, string? title, State state, string orderBy = "EventStartDate", bool desc = false, bool ignorePublicationDate = false); + public IEnumerable GetEvents(DateTime? startDate, DateTime? endDate, IEnumerable? activityAreas, IEnumerable? tags, string? organizerId, string? title, State state, string orderBy = "EventStartDate", bool desc = false, bool ignorePublicationDate = false); public EventResponseDTO GetEvent(Guid id); - public EventResponseDTO AddEvent(Guid userId, EventCreationRequestDTO request); + public EventResponseDTO AddEvent(string userId, EventCreationRequestDTO request); - public bool DeleteEvent(Guid userId, Guid eventId); + public bool DeleteEvent(string userId, Guid eventId); - public bool UpdateEvent(Guid userId, Guid eventId, EventUpdateRequestDTO request); + public bool UpdateEvent(string userId, Guid eventId, EventUpdateRequestDTO request); - public bool UpdateEventState(Guid userId, Guid eventId, State state, string? reason); + public bool UpdateEventState(string userId, Guid eventId, State state, string? reason); public bool UpdateEventReportCount(Guid eventId); diff --git a/core/Services/Abstractions/IUserService.cs b/core/Services/Abstractions/IUserService.cs index be4098c..c487b3e 100644 --- a/core/Services/Abstractions/IUserService.cs +++ b/core/Services/Abstractions/IUserService.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Tracing; +using System.Diagnostics.Tracing; using api.core.Data.Enums; using api.core.Data.requests; @@ -8,17 +8,17 @@ namespace api.core.services.abstractions; public interface IUserService { - public UserResponseDTO AddOrganizer(Guid id, UserCreateDTO organizerDto); + public UserResponseDTO AddOrganizer(string id, UserCreateDTO organizerDto); - public UserResponseDTO GetUser(Guid id); + public UserResponseDTO GetUser(string userToken); - public string GetUserAvatarUrl(Guid id); + public string GetUserAvatarUrl(string id); public IEnumerable GetUsers(string? search, OrganizerAccountActiveFilter activeFilter, out int count); - public bool UpdateUser(Guid id, UserUpdateDTO dto); + public bool UpdateUser(string id, UserUpdateDTO dto); - public bool ToggleUserActiveState(Guid id); + public bool ToggleUserActiveState(string id); - public string UpdateUserAvatar(Guid id, IFormFile avatarFile); + public string UpdateUserAvatar(string id, IFormFile avatarFile); } diff --git a/core/Services/ActivityAreaService.cs b/core/Services/ActivityAreaService.cs index 276969e..fe1b315 100644 --- a/core/Services/ActivityAreaService.cs +++ b/core/Services/ActivityAreaService.cs @@ -14,7 +14,7 @@ public IEnumerable GetAllActivityAreas(string? search) { return activityAreaRepository.GetAll() .Where(aa => - (search.IsNullOrEmpty() || + (search == null || search.Equals("") || aa.NameFr.ToLower().Contains(search!.ToLower() ?? "") || aa.NameEn.ToLower().Contains(search!.ToLower() ?? "")) && aa.DeletedAt == null) diff --git a/core/Services/AuthService.cs b/core/Services/AuthService.cs deleted file mode 100644 index c93358a..0000000 --- a/core/Services/AuthService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using api.core.services.abstractions; - -using Supabase; - -namespace api.core.Services; - -public class AuthService(Client client) : IAuthService -{ - public string SignUp(string email, string password) - { - var user = client.Auth.SignUp(email, password).Result; - - return user?.User?.Id - ?? throw new Exception("An error occured while signing up the user"); - } -} diff --git a/core/Services/DraftEventService.cs b/core/Services/DraftEventService.cs index 73dba04..a923ca4 100644 --- a/core/Services/DraftEventService.cs +++ b/core/Services/DraftEventService.cs @@ -17,19 +17,20 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using api.core.Services.Abstractions; +using api.core.Repositories.Abstractions; namespace api.core.Services; public class DraftEventService( IEventRepository evntRepo, ITagService tagService, - IOrganizerRepository orgRepo, + IUserRepository orgRepo, IFileShareService fileShareService, IImageService imageService) : IDraftEventService { - public EventResponseDTO AddDraftEvent(Guid userId, DraftEventRequestDTO request) + public EventResponseDTO AddDraftEvent(string userId, DraftEventRequestDTO request) { - var organizer = orgRepo.Get(userId) ?? throw new UnauthorizedException(); + var organizer = orgRepo.GetOrganizer(userId) ?? throw new UnauthorizedException(); if (request.Tags.Count > 5) throw new BadParameterException(nameof(request.Tags), "Too many tags"); @@ -67,7 +68,7 @@ public EventResponseDTO AddDraftEvent(Guid userId, DraftEventRequestDTO request) return EventResponseDTO.Map(inserted); } - public bool UpdateDraftEvent(Guid userId, Guid eventId, DraftEventRequestDTO request) + public bool UpdateDraftEvent(string userId, Guid eventId, DraftEventRequestDTO request) { _ = orgRepo.Get(userId) ?? throw new UnauthorizedException(); diff --git a/core/Services/EventService.cs b/core/Services/EventService.cs index ea44fcb..4a2876d 100644 --- a/core/Services/EventService.cs +++ b/core/Services/EventService.cs @@ -13,6 +13,7 @@ using api.core.Extensions; using Microsoft.IdentityModel.Tokens; +using api.core.Repositories.Abstractions; namespace api.core.Services; @@ -21,8 +22,7 @@ public class EventService( IConfiguration config, IEventRepository evntRepo, ITagService tagService, - IOrganizerRepository orgRepo, - IModeratorRepository moderatorRepo, + IUserRepository userRepository, IFileShareService fileShareService, IEmailService emailService, IImageService imageService, @@ -35,7 +35,7 @@ public IEnumerable GetEvents( DateTime? endDate, IEnumerable? activityAreas, IEnumerable? tags, - Guid? organizerId, + string? organizerId, string? title, State state, string orderBy = "EventStartDate", @@ -52,8 +52,8 @@ public IEnumerable GetEvents( (state.HasFlag(e.Publication.State)) && (organizerId == null || e.Publication.OrganizerId == organizerId) && (title == null || (e.Publication.Title != null && e.Publication.Title.ToLower().Contains(title.ToLower()))) && - (tags.IsNullOrEmpty() || e.Publication.Tags.Any(t => tags!.Any(tt => t.Id == tt))) && - (activityAreas.IsNullOrEmpty() || activityAreas!.Any(aa => aa == e.Publication.Organizer.ActivityAreaId))) + (tags == null || tags.Count() < 1 || e.Publication.Tags.Any(t => tags!.Any(tt => t.Id == tt))) && + (activityAreas == null || activityAreas.Count() < 1 || activityAreas!.Any(aa => aa == e.Publication.Organizer.ActivityAreaId))) .AsQueryable() .OrderBy(orderBy, desc) .Select(EventResponseDTO.Map); @@ -67,9 +67,9 @@ public EventResponseDTO GetEvent(Guid id) return EventResponseDTO.Map(evnt!); } - public EventResponseDTO AddEvent(Guid userId, EventCreationRequestDTO request) + public EventResponseDTO AddEvent(string userId, EventCreationRequestDTO request) { - var organizer = orgRepo.Get(userId) ?? throw new UnauthorizedException(); + var organizer = userRepository.GetOrganizer(userId) ?? throw new UnauthorizedException(); if (request.Tags.Count > 5) throw new BadParameterException(nameof(request.Tags), "Too many tags"); @@ -110,9 +110,9 @@ public EventResponseDTO AddEvent(Guid userId, EventCreationRequestDTO request) return EventResponseDTO.Map(inserted); } - public EventResponseDTO AddDraftEvent(Guid userId, DraftEventRequestDTO request) + public EventResponseDTO AddDraftEvent(string userId, DraftEventRequestDTO request) { - var organizer = orgRepo.Get(userId) ?? throw new UnauthorizedException(); + var organizer = userRepository.GetOrganizer(userId) ?? throw new UnauthorizedException(); if (request.Tags.Count > 5) throw new BadParameterException(nameof(request.Tags), "Too many tags"); @@ -155,7 +155,7 @@ public EventResponseDTO AddDraftEvent(Guid userId, DraftEventRequestDTO request) return EventResponseDTO.Map(inserted); } - public bool DeleteEvent(Guid userId, Guid eventId) + public bool DeleteEvent(string userId, Guid eventId) { var eventToDelete = evntRepo.Get(eventId); NotFoundException.ThrowIfNull(eventToDelete); @@ -165,9 +165,9 @@ public bool DeleteEvent(Guid userId, Guid eventId) throw new UnauthorizedException(); } - public bool UpdateEvent(Guid userId, Guid eventId, EventUpdateRequestDTO request) + public bool UpdateEvent(string userId, Guid eventId, EventUpdateRequestDTO request) { - _ = orgRepo.Get(userId) + _ = userRepository.GetOrganizer(userId) ?? throw new UnauthorizedException(); if (request.Tags.Count > 5) @@ -204,9 +204,9 @@ public bool UpdateEvent(Guid userId, Guid eventId, EventUpdateRequestDTO request return evntRepo.Update(eventId, evnt); } - public bool UpdateEventState(Guid userId, Guid eventId, State state, string? reason) + public bool UpdateEventState(string userId, Guid eventId, State state, string? reason) { - var moderator = moderatorRepo.Get(userId) ?? throw new UnauthorizedException(); + var moderator = userRepository.GetModerator(userId) ?? throw new UnauthorizedException(); var evnt = evntRepo.Get(eventId); if (evnt!.Publication.ModeratorId == null) @@ -265,7 +265,7 @@ private void SendEmailStatusChange(Event evnt, string? reason) EmailsUtils.StatusChangeTemplate); } - private static bool CanPerformAction(Guid userId, Event evnt) + private static bool CanPerformAction(string userId, Event evnt) { return (evnt!.Publication.Moderator != null && evnt.Publication.Moderator.Id == userId) || (evnt!.Publication.Organizer != null && evnt.Publication.Organizer.Id == userId); diff --git a/core/Services/ModeratorService.cs b/core/Services/ModeratorService.cs index d72278f..6ed80f3 100644 --- a/core/Services/ModeratorService.cs +++ b/core/Services/ModeratorService.cs @@ -1,12 +1,14 @@ using api.core.data.entities; +using api.core.Data.Entities; using api.core.Data.Exceptions; using api.core.Data.Requests; using api.core.repositories.abstractions; +using api.core.Repositories.Abstractions; using api.core.services.abstractions; namespace api.core.Services; -public class ModeratorService(IModeratorRepository moderatorRepository, IConfiguration configuration) : IModeratorService +public class ModeratorService(IUserRepository userRepository, IConfiguration configuration) : IModeratorService { public ModeratorResponseDTO CreateModerator(string apiKey, ModeratorCreateRequestDTO req) { @@ -18,12 +20,13 @@ public ModeratorResponseDTO CreateModerator(string apiKey, ModeratorCreateReques throw new UnauthorizedException(); // Create the moderator - moderatorRepository.Add(new Moderator + userRepository.Add(new User { Id = req.Id, Email = req.Email, CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Moderator }); return new ModeratorResponseDTO diff --git a/core/Services/UserService.cs b/core/Services/UserService.cs index 9d4f3c0..7b56b99 100644 --- a/core/Services/UserService.cs +++ b/core/Services/UserService.cs @@ -1,31 +1,31 @@ -using api.core.data.entities; +using api.core.data.entities; using api.core.Data.Exceptions; using api.core.Data.Enums; using api.core.Data.requests; using api.core.Data.Responses; -using api.core.repositories; using api.core.repositories.abstractions; using api.core.services.abstractions; using api.files.Services.Abstractions; -using Microsoft.IdentityModel.Tokens; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using api.core.Misc; +using api.core.Repositories.Abstractions; +using api.core.Data.Entities; namespace api.core.Services; public class UserService( - IOrganizerRepository organizerRepository, + IUserRepository userRepository, IFileShareService fileShareService, - IModeratorRepository moderatorRepository, ITagRepository tagRepository, IActivityAreaRepository activityAreaRepository, - IImageService imageService) : IUserService + IImageService imageService, + IJwtUtils jwtUtils) : IUserService { private const string AVATAR_FILE_NAME = "avatar.webp"; - public UserResponseDTO AddOrganizer(Guid id, UserCreateDTO organizerDto) + public UserResponseDTO AddOrganizer(string id, UserCreateDTO organizerDto) { if (organizerDto.ActivityAreaId != null) { @@ -33,7 +33,7 @@ public UserResponseDTO AddOrganizer(Guid id, UserCreateDTO organizerDto) NotFoundException.ThrowIfNull(activityArea); } - var inserted = organizerRepository.Add(new Organizer + var inserted = userRepository.Add(new User { Id = id, Email = organizerDto.Email, @@ -43,39 +43,41 @@ public UserResponseDTO AddOrganizer(Guid id, UserCreateDTO organizerDto) IsActive = true, HasLoggedIn = false, CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Organizer }); var avatarUri = fileShareService.FileGetDownloadUri($"{id}/{AVATAR_FILE_NAME}"); var user = UserResponseDTO.Map(inserted); user.AvatarUrl = avatarUri.ToString(); - + return user; } - public UserResponseDTO GetUser(Guid id) + public UserResponseDTO GetUser(string authHeader) { - UserResponseDTO? userRes = null; - var organizer = organizerRepository.Get(id); + string userId = jwtUtils.GetUserIdFromAuthHeader(authHeader); + UserResponseDTO? userRes = null; + var organizer = userRepository.GetOrganizer(userId); if (organizer != null) userRes = UserResponseDTO.Map(organizer!); - var moderator = moderatorRepository.Get(id); + var moderator = userRepository.GetModerator(userId); if (moderator != null) userRes = UserResponseDTO.Map(moderator!); if (userRes == null) throw new Exception("No users associated with this ID"); - var fields = tagRepository.GetInterestFieldsForOrganizer(id); + var fields = tagRepository.GetInterestFieldsForOrganizer(userId); userRes.FieldsOfInterests = fields; - var avatarUri = fileShareService.FileGetDownloadUri($"{id}/{AVATAR_FILE_NAME}"); + var avatarUri = fileShareService.FileGetDownloadUri($"{userId}/{AVATAR_FILE_NAME}"); userRes.AvatarUrl = avatarUri.ToString(); return userRes; } - public string GetUserAvatarUrl(Guid id) + public string GetUserAvatarUrl(string id) { var avatarUri = fileShareService.FileGetDownloadUri($"{id}/{AVATAR_FILE_NAME}"); return avatarUri.ToString(); @@ -83,8 +85,8 @@ public string GetUserAvatarUrl(Guid id) public IEnumerable GetUsers(string? search, OrganizerAccountActiveFilter activeFilter, out int count) { - var organizers = organizerRepository.GetAll() - .Where(x => (search.IsNullOrEmpty() || + var organizers = userRepository.GetAll() + .Where(x => (search == null || search.Equals("") || x.Organization.ToLower().Contains(search!.ToLower() ?? "") || x.Email.ToLower().Contains(search!.ToLower() ?? "")) && ((activeFilter.HasFlag(OrganizerAccountActiveFilter.Active) && x.IsActive) || @@ -96,16 +98,16 @@ public IEnumerable GetUsers(string? search, OrganizerAccountAct return organizers.Select(UserResponseDTO.Map); } - public bool ToggleUserActiveState(Guid id) + public bool ToggleUserActiveState(string id) { EnsureIsOrganizer(id); - var user = organizerRepository.Get(id); + var user = userRepository.Get(id); user!.IsActive = !user.IsActive; - return organizerRepository.Update(id, user); + return userRepository.Update(id, user); } - private void EnsureIsOrganizer(Guid id) + private void EnsureIsOrganizer(string id) { var user = GetUser(id); @@ -113,7 +115,7 @@ private void EnsureIsOrganizer(Guid id) throw new Exception("Moderators cannot be disabled"); } - public bool UpdateUser(Guid id, UserUpdateDTO dto) + public bool UpdateUser(string id, UserUpdateDTO dto) { var user = GetUser(id); @@ -125,14 +127,14 @@ public bool UpdateUser(Guid id, UserUpdateDTO dto) return user.Type switch { - "Moderator" => moderatorRepository.Update(id, new Moderator + "Moderator" => userRepository.Update(id, new User { Id = id, Email = dto.Email, CreatedAt = user.CreatedAt, UpdatedAt = DateTime.UtcNow }), - "Organizer" => organizerRepository.Update(id, new Organizer + "Organizer" => userRepository.Update(id, new User { Id = id, Email = dto.Email, @@ -156,7 +158,7 @@ public bool UpdateUser(Guid id, UserUpdateDTO dto) }; } - public string UpdateUserAvatar(Guid id, IFormFile avatarFile) + public string UpdateUserAvatar(string id, IFormFile avatarFile) { _ = GetUser(id); var userId = id.ToString(); diff --git a/core/api.core.csproj b/core/api.core.csproj index 03ba97c..d02811e 100644 --- a/core/api.core.csproj +++ b/core/api.core.csproj @@ -17,23 +17,26 @@ - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + - + - - + diff --git a/core/appsettings.Development.json b/core/appsettings.Development.json index 0c208ae..d3831e8 100644 --- a/core/appsettings.Development.json +++ b/core/appsettings.Development.json @@ -1,8 +1,17 @@ { + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "188c27a3-86bf-4988-9c94-025a75fcf0d1", + "ClientId": "bf42ef76-b599-4ab1-a015-6e4b8afa347b", + "Issuer": "https://login.microsoftonline.com/188c27a3-86bf-4988-9c94-025a75fcf0d1/v2.0", + "Scopes": "User.Read", + "Audience": "bf42ef76-b599-4ab1-a015-6e4b8afa347b" + }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "AllowedHosts": "*" } diff --git a/core/appsettings.json b/core/appsettings.json index 10f68b8..d3831e8 100644 --- a/core/appsettings.json +++ b/core/appsettings.json @@ -1,4 +1,12 @@ { + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "188c27a3-86bf-4988-9c94-025a75fcf0d1", + "ClientId": "bf42ef76-b599-4ab1-a015-6e4b8afa347b", + "Issuer": "https://login.microsoftonline.com/188c27a3-86bf-4988-9c94-025a75fcf0d1/v2.0", + "Scopes": "User.Read", + "Audience": "bf42ef76-b599-4ab1-a015-6e4b8afa347b" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9c6fa6a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,42 @@ +version: '3.9' +services: + backend: + image: ghcr.io/applets/backend-hello:main + restart: always + ports: + - 8080:8080 + env_file: + - core/.env + volumes: + - ./backend-data:/app/volume:rw + adminer: + image: adminer + restart: always + ports: + - 8090:8080 + db: + image: postgres + restart: always + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=test123 + env_file: + - core/.env + redis: + image: redis:7.2.4 + restart: always + ports: + - '63790:6379' + command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + volumes: + - cache:/data + cdn: + image: nginx:1.25.3 + restart: always + ports: + - 6464:80 +volumes: + cache: + driver: local + \ No newline at end of file diff --git a/emails/api.emails.csproj b/emails/api.emails.csproj index a357510..37421d2 100644 --- a/emails/api.emails.csproj +++ b/emails/api.emails.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/files/api.files.csproj b/files/api.files.csproj index 8023c22..1cc7c7e 100644 --- a/files/api.files.csproj +++ b/files/api.files.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/tests/Tests/Services/EventServiceTests.cs b/tests/Tests/Services/EventServiceTests.cs index 27eb89b..f20acc5 100644 --- a/tests/Tests/Services/EventServiceTests.cs +++ b/tests/Tests/Services/EventServiceTests.cs @@ -1,8 +1,10 @@ using api.core.data.entities; +using api.core.Data.Entities; using api.core.Data.Enums; using api.core.Data.Exceptions; using api.core.Data.requests; using api.core.repositories.abstractions; +using api.core.Repositories.Abstractions; using api.core.services.abstractions; using api.core.Services; using api.core.Services.Abstractions; @@ -46,20 +48,22 @@ public class EventServiceTests Name = "Test" } ], - Organizer = new Organizer + Organizer = new User { - Id = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), ActivityAreaId = ActivityAreaClubId, - ActivityArea = new ActivityArea - { - Id = ActivityAreaClubId, - NameEn = "Club", - NameFr = "Club" - } + //ActivityArea = new ActivityArea + //{ + // Id = ActivityAreaClubId, + // NameEn = "Club", + // NameFr = "Club" + //}, + Role = UserRole.Organizer }, - Moderator = new Moderator + Moderator = new User { - Id = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), + Role = UserRole.Moderator }, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now @@ -85,16 +89,17 @@ public class EventServiceTests Name = "Test" } ], - Organizer = new Organizer + Organizer = new User { - Id = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), ActivityAreaId = ActivityAreaSchoolId, - ActivityArea = new ActivityArea - { - Id = ActivityAreaSchoolId, - NameEn = "School", - NameFr = "School" - } + //ActivityArea = new ActivityArea + //{ + // Id = ActivityAreaSchoolId, + // NameEn = "School", + // NameFr = "School" + //} + Role = UserRole.Organizer }, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now @@ -113,16 +118,16 @@ public class EventServiceTests State = State.Published, PublicationDate = DateTime.UtcNow, Tags = [], - Organizer = new Organizer + Organizer = new User { - Id = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), ActivityAreaId = ActivityAreaSchoolId, - ActivityArea = new ActivityArea - { - Id = ActivityAreaSchoolId, - NameEn = "School", - NameFr = "School" - } + //ActivityArea = new ActivityArea + //{ + // Id = ActivityAreaSchoolId, + // NameEn = "School", + // NameFr = "School" + //} }, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now @@ -148,16 +153,16 @@ public class EventServiceTests Name = "Test" } ], - Organizer = new Organizer + Organizer = new User { - Id = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), ActivityAreaId = ActivityAreaSchoolId, - ActivityArea = new ActivityArea - { - Id = ActivityAreaSchoolId, - NameEn = "School", - NameFr = "School" - } + //ActivityArea = new ActivityArea + //{ + // Id = ActivityAreaSchoolId, + // NameEn = "School", + // NameFr = "School" + //} }, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, @@ -169,8 +174,7 @@ public class EventServiceTests private readonly Mock _mockEventRepository; private readonly Mock _mockTagService; - private readonly Mock _mockOrganizerRepository; - private readonly Mock _mockModeratorRepository; + private readonly Mock _mockUserRepository; private readonly Mock _mockFileShareService; private readonly Mock _mockEmailService; private readonly Mock _mockImageService; @@ -181,8 +185,7 @@ public EventServiceTests() { _mockEventRepository = new Mock(); _mockTagService = new Mock(); - _mockOrganizerRepository = new Mock(); - _mockModeratorRepository = new Mock(); + _mockUserRepository = new Mock(); _mockFileShareService = new Mock(); _mockEmailService = new Mock(); _mockImageService = new Mock(); @@ -194,8 +197,7 @@ public EventServiceTests() _mockConfig.Object, _mockEventRepository.Object, _mockTagService.Object, - _mockOrganizerRepository.Object, - _mockModeratorRepository.Object, + _mockUserRepository.Object, _mockFileShareService.Object, _mockEmailService.Object, _mockImageService.Object, @@ -335,11 +337,11 @@ public void GetEvent_ShouldReturnEvent() public void AddEvents_ShouldThrowAnExceptionWhenOrganizerIsUnknown() { // Arrange - _mockOrganizerRepository.Setup(repo => repo.Get(It.IsAny())).Returns((Organizer?)null); + _mockUserRepository.Setup(repo => repo.GetOrganizer(It.IsAny())).Returns((User?)null); // Act _eventService.Invoking(s => - s.AddEvent(Guid.Empty, new EventCreationRequestDTO())) + s.AddEvent("", new EventCreationRequestDTO())) .Should().Throw(); } @@ -371,7 +373,7 @@ public void DeleteEvent_ShouldThrowNotFoundException_WhenEventDoesNotExist() _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns((Event?)null); // Act - Action act = () => _eventService.DeleteEvent(userId, eventId); + Action act = () => _eventService.DeleteEvent("", eventId); // Assert act.Should().Throw>(); @@ -382,7 +384,7 @@ public void DeleteEvent_ShouldThrowNotFoundException_WhenEventDoesNotExist() public void DeleteEvent_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized() { // Arrange - var unauthorizedUserId = Guid.NewGuid(); + var unauthorizedUserId = "unauthorized-user"; var eventId = _events.First().Id; _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(_events.First()); @@ -420,14 +422,13 @@ public void UpdateEvent_ShouldReturnTrue_WhenEventIsUpdatedSuccessfully() _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(_events.First()); _mockEventRepository.Setup(repo => repo.Update(eventId, It.IsAny())).Returns(true); - _mockOrganizerRepository.Setup(repo => repo.Get(It.IsAny())).Returns(new Organizer { Id = userId }); + _mockUserRepository.Setup(repo => repo.GetOrganizer(It.IsAny())).Returns(new User { Id = userId, Role = UserRole.Organizer }); _eventService = new EventService( _mockConfig.Object, _mockEventRepository.Object, _mockTagService.Object, - _mockOrganizerRepository.Object, - _mockModeratorRepository.Object, + _mockUserRepository.Object, _mockFileShareService.Object, _mockEmailService.Object, _mockImageService.Object, @@ -445,7 +446,7 @@ public void UpdateEvent_ShouldReturnTrue_WhenEventIsUpdatedSuccessfully() public void UpdateEvent_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized() { // Arrange - var unauthorizedUserId = Guid.NewGuid(); + var unauthorizedUserId = "unauthorized-user"; var eventId = _events.First().Id; var request = new EventUpdateRequestDTO @@ -488,7 +489,7 @@ public void UpdateEventState_ShouldReturnTrue_WhenStateIsUpdatedSuccessfullyByMo _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(eventToUpdate); _mockEventRepository.Setup(repo => repo.Update(eventId, It.IsAny())).Returns(true); - _mockModeratorRepository.Setup(repo => repo.Get(It.IsAny())).Returns(new Moderator { Id = userId }); + _mockUserRepository.Setup(repo => repo.GetModerator(It.IsAny())).Returns(new User { Id = userId, Role = UserRole.Moderator }); _mockEmailService.Setup(service => service.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); // Act @@ -513,7 +514,7 @@ public void UpdateEventState_ShouldHaveStatePublished_WhenStateIsUpdatedWithAppr _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(eventToUpdate); _mockEventRepository.Setup(repo => repo.Update(eventId, It.IsAny())).Returns(true); - _mockModeratorRepository.Setup(repo => repo.Get(It.IsAny())).Returns(new Moderator { Id = userId }); + _mockUserRepository.Setup(repo => repo.GetModerator(It.IsAny())).Returns(new User { Id = userId, Role = UserRole.Moderator }); _mockEmailService.Setup(service => service.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); // Act @@ -532,12 +533,12 @@ public void UpdateEventState_ShouldHaveStatePublished_WhenStateIsUpdatedWithAppr public void UpdateEventState_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized() { // Arrange - var unauthorizedUserId = Guid.NewGuid(); + var unauthorizedUserId = "unauthorized-user"; var eventId = _events.First().Id; var newState = State.Approved; var eventToUpdate = _events.First(); - eventToUpdate.Publication.ModeratorId = Guid.NewGuid(); + eventToUpdate.Publication.ModeratorId = "moderator-id"; _mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(eventToUpdate); diff --git a/tests/Tests/Services/NotificationServiceTests.cs b/tests/Tests/Services/NotificationServiceTests.cs index 4b6dbbd..be668a1 100644 --- a/tests/Tests/Services/NotificationServiceTests.cs +++ b/tests/Tests/Services/NotificationServiceTests.cs @@ -9,6 +9,7 @@ using api.emails.Models; using Microsoft.Extensions.Configuration; using api.emails; +using api.core.Data.Entities; namespace api.tests.Tests.Services; @@ -53,7 +54,7 @@ public void BulkAddNotificationForPublication_ShouldAdd2NotifsForPub() { // Arrange var pubId = Guid.NewGuid(); - var orgId = Guid.NewGuid(); + var orgId = "org"; _mockEventRepository.Setup(x => x.GetAll()).Returns(new List { new Event @@ -131,8 +132,8 @@ public void BulkAddNotificationForPublication_ShouldAddNoneWhenSubscriptionOnOth { // Arrange var pubId = Guid.NewGuid(); - var orgId = Guid.NewGuid(); - var otherOrgId = Guid.NewGuid(); + var orgId = "org"; + var otherOrgId = "otherOrg"; _mockEventRepository.Setup(x => x.GetAll()).Returns(new List { new Event @@ -176,7 +177,8 @@ public void BulkAddNotificationForPublication_ShouldCleanIsSentNotif() { // Arrange var pubId = Guid.NewGuid(); - var orgId = Guid.NewGuid(); + var subId = Guid.NewGuid(); + var orgId = "org"; _mockEventRepository.Setup(x => x.GetAll()).Returns(new List { new Event @@ -193,7 +195,7 @@ public void BulkAddNotificationForPublication_ShouldCleanIsSentNotif() { new Subscription { - Id = Guid.NewGuid(), + Id = subId, OrganizerId = orgId, DeletedAt = null, CreatedAt = DateTime.UtcNow @@ -204,7 +206,7 @@ public void BulkAddNotificationForPublication_ShouldCleanIsSentNotif() new Notification { Id = Guid.NewGuid(), - SubscriptionId = orgId, + SubscriptionId = subId, PublicationId = pubId, IsSent = true, DeletedAt = null, @@ -228,28 +230,30 @@ public async Task SendNewsForRemainingPublication_ShouldSendNotificationWaiting( // Arrange var notifId = Guid.NewGuid(); var pubId = Guid.NewGuid(); - var orgId = Guid.NewGuid(); + var subId = Guid.NewGuid(); + var orgId = "org"; _mockNotifRepository.Setup(x => x.GetAll()).Returns(new List { new Notification { Id = notifId, - SubscriptionId = orgId, + SubscriptionId = subId, Subscription = new Subscription { - Id = orgId, + Id = subId, OrganizerId = orgId, - SubscriptionToken = "token" + SubscriptionToken = "token", }, PublicationId = pubId, Publication = new Publication { Id = pubId, OrganizerId = orgId, - Organizer = new Organizer + Organizer = new User { Id = orgId, Email = "blabla@bla.com", + Role = UserRole.Organizer } }, IsSent = false, @@ -284,16 +288,17 @@ public async Task SendNewsForRemainingPublication_ShouldReturn0Early() { // Arrange var pubId = Guid.NewGuid(); - var orgId = Guid.NewGuid(); + var subId = Guid.NewGuid(); + var orgId = "org"; _mockNotifRepository.Setup(x => x.GetAll()).Returns(new List { new Notification { Id = Guid.NewGuid(), - SubscriptionId = orgId, + SubscriptionId = subId, Subscription = new Subscription { - Id = orgId, + Id = subId, OrganizerId = orgId, SubscriptionToken = "token" }, @@ -302,10 +307,11 @@ public async Task SendNewsForRemainingPublication_ShouldReturn0Early() { Id = pubId, OrganizerId = orgId, - Organizer = new Organizer + Organizer = new User { Id = orgId, Email = "blabla@bla.com", + Role = UserRole.Organizer } }, IsSent = true, // Won't be sent twice diff --git a/tests/Tests/Services/UserServiceTests.cs b/tests/Tests/Services/UserServiceTests.cs index 0e24441..0e7ce00 100644 --- a/tests/Tests/Services/UserServiceTests.cs +++ b/tests/Tests/Services/UserServiceTests.cs @@ -11,35 +11,39 @@ using System.Diagnostics; using api.core.Data.Exceptions; using api.core.services.abstractions; +using api.core.Data.Entities; +using api.core.Repositories.Abstractions; +using Microsoft.IdentityModel.JsonWebTokens; +using api.core.Misc; namespace api.tests.Tests.Services; public class UserServiceTests { - private readonly Mock _organizerRepositoryMock; - private readonly Mock _moderatorRepositoryMock; + private readonly Mock _userRepositoryMock; private readonly Mock _activityAreaRepositoryMock; private readonly Mock _tagRepositoryMock; private readonly Mock _fileShareServiceMock; private readonly Mock _imageServiceMock; + private readonly Mock _jwtUtilsMock; private readonly UserService _userService; public UserServiceTests() { - _organizerRepositoryMock = new Mock(); - _moderatorRepositoryMock = new Mock(); + _userRepositoryMock = new Mock(); _tagRepositoryMock = new Mock(); _activityAreaRepositoryMock = new Mock(); _fileShareServiceMock = new Mock(); _imageServiceMock = new Mock(); + _jwtUtilsMock = new Mock(); _fileShareServiceMock.Setup(service => service.FileGetDownloadUri(It.IsAny())).Returns(new Uri("http://example.com/avatar.webp")); _userService = new UserService( - _organizerRepositoryMock.Object, + _userRepositoryMock.Object, _fileShareServiceMock.Object, - _moderatorRepositoryMock.Object, _tagRepositoryMock.Object, _activityAreaRepositoryMock.Object, - _imageServiceMock.Object); + _imageServiceMock.Object, + _jwtUtilsMock.Object); } [Fact] @@ -51,7 +55,8 @@ public void AddOrganizer_ShouldReturnUserResponseDTO_WhenOrganizerIsAddedSuccess { Email = "john.doe@example.com", Organization = "ExampleOrg", - ActivityAreaId = actAreaModified + ActivityAreaId = actAreaModified, + Id = "1234" }; var activity = new ActivityArea @@ -59,7 +64,7 @@ public void AddOrganizer_ShouldReturnUserResponseDTO_WhenOrganizerIsAddedSuccess Id = actAreaModified, NameFr = "Tech", }; - var organizer = new Organizer + var organizer = new User { Email = organizerDto.Email, Organization = organizerDto.Organization, @@ -70,11 +75,10 @@ public void AddOrganizer_ShouldReturnUserResponseDTO_WhenOrganizerIsAddedSuccess }; _activityAreaRepositoryMock.Setup(repo => repo.Get(It.IsAny())).Returns(activity); - - _organizerRepositoryMock.Setup(repo => repo.Add(It.IsAny())).Returns(organizer); + _userRepositoryMock.Setup(repo => repo.Add(It.IsAny())).Returns(organizer); // Act - var result = _userService.AddOrganizer(Guid.NewGuid(), organizerDto); + var result = _userService.AddOrganizer("1234", organizerDto); // Assert result.Should().NotBeNull(); @@ -82,15 +86,15 @@ public void AddOrganizer_ShouldReturnUserResponseDTO_WhenOrganizerIsAddedSuccess result.Organization.Should().Be(organizerDto.Organization); result.ActivityArea.Id.ToString().Should().Be(actAreaModified.ToString()); - _organizerRepositoryMock.Verify(repo => repo.Add(It.IsAny()), Times.Once); + _userRepositoryMock.Verify(repo => repo.Add(It.IsAny()), Times.Once); } [Fact] public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById() { // Arrange - var organizerId = Guid.NewGuid(); - var organizer = new Organizer + var organizerId = "organizer"; + var organizer = new User { Id = organizerId, Email = "john.doe@example.com", @@ -104,10 +108,12 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById() UpdatedAt = DateTime.UtcNow }, CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Organizer }; - _organizerRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(organizer); + _userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(organizer); + _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId); // Act var result = _userService.GetUser(organizerId); @@ -117,24 +123,26 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById() result.Id.Should().Be(organizerId); result.Email.Should().Be(organizer.Email); - _organizerRepositoryMock.Verify(repo => repo.Get(organizerId), Times.Once); + _userRepositoryMock.Verify(repo => repo.GetOrganizer(organizerId), Times.Once); } [Fact] public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById() { // Arrange - var moderatorId = Guid.NewGuid(); - var moderator = new Moderator + var moderatorId = "Moderator"; + var moderator = new User { Id = moderatorId, Email = "jane.doe@example.com", CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Moderator }; - _organizerRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns((Organizer?)null); // Simulate no organizer found - _moderatorRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(moderator); // Simulate moderator found + _jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId); + _userRepositoryMock.Setup(repo => repo.GetOrganizer(moderatorId)).Returns((User?)null); // Simulate no organizer found + _userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(moderator); // Simulate moderator found // Act var result = _userService.GetUser(moderatorId); @@ -144,27 +152,29 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById() result.Id.Should().Be(moderatorId); result.Email.Should().Be(moderator.Email); - _organizerRepositoryMock.Verify(repo => repo.Get(moderatorId), Times.Once); - _moderatorRepositoryMock.Verify(repo => repo.Get(moderatorId), Times.Once); + _userRepositoryMock.Verify(repo => repo.GetOrganizer(moderatorId), Times.Once); + _userRepositoryMock.Verify(repo => repo.GetModerator(moderatorId), Times.Once); + _jwtUtilsMock.Verify(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId), Times.Once); } [Fact] public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedId() { // Arrange - var userId = Guid.NewGuid(); + var userId = "nobody"; // Setup both organizer and moderator repositories to return null, simulating that no user is found with the provided ID - _organizerRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as Organizer); - _moderatorRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as Moderator); + _userRepositoryMock.Setup(repo => repo.GetOrganizer(userId)).Returns(null as User); + _userRepositoryMock.Setup(repo => repo.GetModerator(userId)).Returns(null as User); + _jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(userId)).Returns(userId); // Act Action act = () => _userService.GetUser(userId); // Assert act.Should().Throw().WithMessage("No users associated with this ID"); - _organizerRepositoryMock.Verify(repo => repo.Get(userId), Times.Once); - _moderatorRepositoryMock.Verify(repo => repo.Get(userId), Times.Once); + _userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once); + _userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once); } @@ -172,13 +182,14 @@ public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedId() public void UpdateUser_ShouldReturnTrue_WhenOrganizerIsUpdatedSuccessfully() { // Arrange - var organizerId = Guid.NewGuid(); + var organizerId = "jane-doe"; var actAreaIdModified = Guid.NewGuid(); var updateDto = new UserUpdateDTO { Email = "jane.doe@example.com", Organization = "NewOrg", - ActivityAreaId = actAreaIdModified + ActivityAreaId = actAreaIdModified, + Id = organizerId }; var activity = new ActivityArea @@ -187,19 +198,20 @@ public void UpdateUser_ShouldReturnTrue_WhenOrganizerIsUpdatedSuccessfully() NameFr = "Tech", }; - var existingOrganizer = new Organizer + var existingOrganizer = new User { Id = organizerId, Email = "john.doe@example.com", Organization = "ExampleOrg", - ActivityArea = activity, CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Organizer }; _activityAreaRepositoryMock.Setup(repo => repo.Get(actAreaIdModified)).Returns(activity); // Simulate activity area found - _organizerRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer); - _organizerRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny())).Returns(true); + _userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer); + _userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny())).Returns(true); + _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId); // Act var result = _userService.UpdateUser(organizerId, updateDto); @@ -207,23 +219,24 @@ public void UpdateUser_ShouldReturnTrue_WhenOrganizerIsUpdatedSuccessfully() // Assert result.Should().BeTrue(); - _organizerRepositoryMock.Verify(repo => repo.Update(organizerId, It.IsAny()), Times.Once); + _userRepositoryMock.Verify(repo => repo.Update(organizerId, It.IsAny()), Times.Once); } [Fact] public void UpdateUser_ShouldThrow_WhenActivityAreaIsNotFoundInTheList() { // Arrange - var organizerId = Guid.NewGuid(); + var organizerId = "org"; var badActAreaIdModified = Guid.NewGuid(); var updateDto = new UserUpdateDTO { Email = "jane.doe@example.com", Organization = "NewOrg", - ActivityAreaId = badActAreaIdModified + ActivityAreaId = badActAreaIdModified, + Id = organizerId }; - var existingOrganizer = new Organizer + var existingOrganizer = new User { Id = organizerId, Email = "john.doe@example.com", @@ -237,8 +250,9 @@ public void UpdateUser_ShouldThrow_WhenActivityAreaIsNotFoundInTheList() UpdatedAt = DateTime.UtcNow }; _activityAreaRepositoryMock.Setup(repo => repo.Get(badActAreaIdModified)).Returns(null as ActivityArea); // Simulate activity area not found - _organizerRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer); - _organizerRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny())).Returns(true); + _userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer); + _userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny())).Returns(true); + _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId); // Act & Assert Assert.Throws>(() => _userService.UpdateUser(organizerId, updateDto)); @@ -249,22 +263,25 @@ public void UpdateUser_ShouldThrow_WhenActivityAreaIsNotFoundInTheList() public void UpdateUser_ShouldReturnTrue_WhenModeratorIsUpdatedSuccessfully() { // Arrange - var moderatorId = Guid.NewGuid(); + var moderatorId = "mod"; var updateDto = new UserUpdateDTO { - Email = "john.updated@example.com" + Email = "john.updated@example.com", + Id = moderatorId }; - var existingModerator = new Moderator + var existingModerator = new User { Id = moderatorId, Email = "john.doe@example.com", CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Role = UserRole.Moderator }; - _moderatorRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(existingModerator); // Simulate moderator found - _moderatorRepositoryMock.Setup(repo => repo.Update(moderatorId, It.IsAny())).Returns(true); + _userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(existingModerator); // Simulate moderator found + _userRepositoryMock.Setup(repo => repo.Update(moderatorId, It.IsAny())).Returns(true); + _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId); // Act var result = _userService.UpdateUser(moderatorId, updateDto); @@ -272,6 +289,6 @@ public void UpdateUser_ShouldReturnTrue_WhenModeratorIsUpdatedSuccessfully() // Assert result.Should().BeTrue(); - _moderatorRepositoryMock.Verify(repo => repo.Update(moderatorId, It.IsAny()), Times.Once); + _userRepositoryMock.Verify(repo => repo.Update(moderatorId, It.IsAny()), Times.Once); } }