diff --git a/API/Controllers/TacticsController.cs b/API/Controllers/TacticsController.cs index fadb2d2..e67aaf8 100644 --- a/API/Controllers/TacticsController.cs +++ b/API/Controllers/TacticsController.cs @@ -150,4 +150,5 @@ public class TacticController(ITacticService service, IContextAccessor accessor) var found = await service.SetTacticStepContent(tacticId, stepId, JsonSerializer.Serialize(req.Content)); return found ? Ok() : NotFound(); } + } \ No newline at end of file diff --git a/API/Controllers/TeamsController.cs b/API/Controllers/TeamsController.cs index c7a3ab3..7ef3ca6 100644 --- a/API/Controllers/TeamsController.cs +++ b/API/Controllers/TeamsController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using API.Context; using API.Validation; +using AppContext.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Model; @@ -10,7 +11,7 @@ namespace API.Controllers; [ApiController] [Authorize] -public class TeamsController(ITeamService service, IContextAccessor accessor) : ControllerBase +public class TeamsController(ITeamService service, ITacticService tactics,IContextAccessor accessor) : ControllerBase { public record CreateTeamRequest( [Name] string Name, @@ -134,4 +135,51 @@ public class TeamsController(ITeamService service, IContextAccessor accessor) : return Problem(); } } + + public record ShareTacticToTeamRequest( + int TacticId, + int TeamId + ); + + [HttpPost("/team/share-tactic")] + public async Task ShareTactic([FromBody] ShareTacticToTeamRequest sharedTactic) + { + var userId = accessor.CurrentUserId(HttpContext); + var success = await tactics.ShareTactic(sharedTactic.TacticId, null, sharedTactic.TeamId); + + return success ? Ok() : BadRequest(); + } + + [HttpDelete("/tactics/shared/{tacticId:int}/team/{teamId:int}")] + public async Task UnshareTactic(int tacticId, int teamId) + { + var currentUserId = accessor.CurrentUserId(HttpContext); + var tactic = await tactics.GetTactic(tacticId); + + if (tactic == null) + { + return NotFound(); + } + if (currentUserId != tactic.OwnerId) + { + return Unauthorized(); + } + + var success = await tactics.UnshareTactic(tacticId, null, teamId); + return success ? Ok() : NotFound(); + } + + [HttpGet("/tactics/shared/team/{teamId:int}")] + public async Task GetSharedTacticsToTeam(int teamId) + { + var currentUserId = accessor.CurrentUserId(HttpContext); + + if (!await service.IsUserInTeam(currentUserId, teamId)) + { + return Unauthorized(); + } + + var sharedTactics = await service.GetSharedTacticsToTeam(teamId); + return sharedTactics != null ? Ok(sharedTactics) : NotFound(); + } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 2bb6a4f..ea73e3a 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using API.Context; using API.DTO; +using AppContext.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Model; @@ -35,4 +36,64 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ var userTactics = await tactics.ListTacticsOf(userId); return new GetUserDataResponse(userTeams.ToArray(), userTactics.Select(t => t.ToDto()).ToArray()); } + + public record ShareTacticToUserRequest( + int TacticId, + int UserId + ); + + [HttpPost("/user/share-tactic")] + [Authorize] + public async Task ShareTactic([FromBody] ShareTacticToUserRequest sharedTactic) + { + var currentUserId = accessor.CurrentUserId(HttpContext); + var tactic = await tactics.GetTactic(sharedTactic.TacticId); + + if (tactic == null) + { + return NotFound(); + } + + if (currentUserId != tactic.OwnerId) + { + return Unauthorized(); + } + + var result = await tactics.ShareTactic(sharedTactic.TacticId, sharedTactic.UserId, null); + return result ? Ok() : NotFound(); + } + + [HttpDelete("/tactics/shared/{tacticId:int}/user/{userId:int}")] + [Authorize] + public async Task UnshareTactic(int tacticId, int userId) + { + var currentUserId = accessor.CurrentUserId(HttpContext); + var tactic = await tactics.GetTactic(tacticId); + + if (tactic == null) + { + return NotFound(); + } + if (currentUserId != tactic.OwnerId) + { + return Unauthorized(); + } + + var success = await tactics.UnshareTactic(tacticId, userId, null); + return success ? Ok() : NotFound(); + } + + [HttpGet("/tactics/shared/user/{userId:int}")] + [Authorize] + public async Task GetSharedTacticsToUser(int userId) + { + var currentUserId = accessor.CurrentUserId(HttpContext); + if (currentUserId != userId) + { + return Unauthorized(); + } + + var sharedTactics = await users.GetSharedTacticsToUser(userId); + return sharedTactics != null ? Ok(sharedTactics) : NotFound(); + } } \ No newline at end of file diff --git a/APIConsole/TeamsControllerConsole.cs b/APIConsole/TeamsControllerConsole.cs index d9f993e..e4ede67 100644 --- a/APIConsole/TeamsControllerConsole.cs +++ b/APIConsole/TeamsControllerConsole.cs @@ -16,8 +16,9 @@ namespace APIConsole { AppContext.AppContext context = new AppContext.AppContext(); ITeamService teams = new DbTeamService(context); + ITacticService tactics = new DbTacticService(context); IContextAccessor accessor = new HttpContextAccessor(); - _controller = new TeamsController(teams, accessor); + _controller = new TeamsController(teams, tactics, accessor); } public async void GetMembersOfTest() diff --git a/AppContext/AppContext.cs b/AppContext/AppContext.cs index 5a2eb8f..c6d2618 100644 --- a/AppContext/AppContext.cs +++ b/AppContext/AppContext.cs @@ -13,7 +13,9 @@ public class AppContext : DbContext public DbSet Teams { get; init; } public DbSet Members { get; init; } public DbSet TacticSteps { get; set; } + public DbSet SharedTactics { get; set; } + public AppContext() { diff --git a/AppContext/Entities/SharedTacticEntity.cs b/AppContext/Entities/SharedTacticEntity.cs new file mode 100644 index 0000000..852dd92 --- /dev/null +++ b/AppContext/Entities/SharedTacticEntity.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace AppContext.Entities +{ + public class SharedTacticEntity + { + [Key] public int Id { get; set; } + public int TacticId { get; set; } + public int? SharedWithUserId { get; set; } + public int? SharedWithTeamId { get; set; } + } +} \ No newline at end of file diff --git a/Converters/ModelToEntities.cs b/Converters/ModelToEntities.cs index 6c8006c..001aed6 100644 --- a/Converters/ModelToEntities.cs +++ b/Converters/ModelToEntities.cs @@ -37,4 +37,9 @@ public static class EntitiesToModels { return new Member(entity.TeamId, entity.UserId, entity.Role); } + + public static SharedTactic ToModel(this SharedTacticEntity entity) + { + return new SharedTactic(entity.Id, entity.TacticId, entity.SharedWithUserId, entity.SharedWithTeamId); + } } \ No newline at end of file diff --git a/DbServices/DbTacticService.cs b/DbServices/DbTacticService.cs index c9c1df3..0d4a3ba 100644 --- a/DbServices/DbTacticService.cs +++ b/DbServices/DbTacticService.cs @@ -159,4 +159,40 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService return await context.SaveChangesAsync() > 0; } + + public async Task ShareTactic(int tacticId, int? userId, int? teamId) + { + var sharedTactic = new SharedTacticEntity + { + TacticId = tacticId, + SharedWithUserId = userId, + SharedWithTeamId = teamId + }; + + await context.SharedTactics.AddAsync(sharedTactic); + return await context.SaveChangesAsync() > 0; + } + + public async Task UnshareTactic(int tacticId, int? userId, int? teamId) + { + SharedTacticEntity? sharedTactic = null; + if (userId.HasValue) + { + sharedTactic = await context.SharedTactics + .FirstOrDefaultAsync(st => st.TacticId == tacticId && st.SharedWithUserId == userId); + } + else if (teamId.HasValue) + { + sharedTactic = await context.SharedTactics + .FirstOrDefaultAsync(st => st.TacticId == tacticId && st.SharedWithTeamId == teamId); + } + + if (sharedTactic == null) + { + return false; + } + + context.SharedTactics.Remove(sharedTactic); + return await context.SaveChangesAsync() > 0; + } } \ No newline at end of file diff --git a/DbServices/DbTeamService.cs b/DbServices/DbTeamService.cs index 87fa2bd..8d32533 100644 --- a/DbServices/DbTeamService.cs +++ b/DbServices/DbTeamService.cs @@ -73,6 +73,29 @@ public class DbTeamService(AppContext.AppContext context) : ITeamService return await context.SaveChangesAsync() > 0; } + public async Task> GetSharedTacticsToTeam(int teamId) + { + var sharedTactics = await context.SharedTactics + .Where(st => st.SharedWithTeamId == teamId) + .ToListAsync(); + + var tactics = new List(); + foreach (var sharedTactic in sharedTactics) + { + var tactic = await context.Tactics + .Where(t => t.Id == sharedTactic.TacticId) + .Select(t => t.ToModel()) + .FirstOrDefaultAsync(); + + if (tactic != null) + { + tactics.Add(tactic); + } + } + + return tactics; + } + public Task> GetMembersOf(int teamId) { @@ -131,4 +154,10 @@ public class DbTeamService(AppContext.AppContext context) : ITeamService ? ITeamService.TeamAccessibility.Authorized : ITeamService.TeamAccessibility.Unauthorized; } + + public async Task IsUserInTeam(int userId, int teamId) + { + return await context.Members + .AnyAsync(m => m.TeamId == teamId && m.UserId == userId); + } } \ No newline at end of file diff --git a/DbServices/DbUserService.cs b/DbServices/DbUserService.cs index 62c2a8a..8cdbac5 100644 --- a/DbServices/DbUserService.cs +++ b/DbServices/DbUserService.cs @@ -95,4 +95,27 @@ public class DbUserService(AppContext.AppContext context) : IUserService .FirstOrDefaultAsync(u => u.Email == email)) ?.ToModel(); } + + public async Task> GetSharedTacticsToUser(int userId) + { + var sharedTactics = await context.SharedTactics + .Where(st => st.SharedWithUserId == userId) + .ToListAsync(); + + var tactics = new List(); + foreach (var sharedTactic in sharedTactics) + { + var tactic = await context.Tactics + .Where(t => t.Id == sharedTactic.TacticId) + .Select(t => t.ToModel()) + .FirstOrDefaultAsync(); + + if (tactic != null) + { + tactics.Add(tactic); + } + } + + return tactics; + } } \ No newline at end of file diff --git a/Model/SharedTactic.cs b/Model/SharedTactic.cs new file mode 100644 index 0000000..8fc29e8 --- /dev/null +++ b/Model/SharedTactic.cs @@ -0,0 +1,3 @@ +namespace Model; + +public record SharedTactic(int Id, int TacticId, int? SharedWithUserId, int? SharedWithTeamId); \ No newline at end of file diff --git a/Services/ITacticService.cs b/Services/ITacticService.cs index 9f443ac..99c3b80 100644 --- a/Services/ITacticService.cs +++ b/Services/ITacticService.cs @@ -56,6 +56,10 @@ public interface ITacticService /// A task that represents the asynchronous operation. The task result contains the JSON content of the step. Task GetTacticStepContent(int tacticId, int stepId); + + public Task ShareTactic(int tacticId, int? userId, int? teamId); + public Task UnshareTactic(int tacticId, int? userId, int? teamId); + /// /// Retrieves the root step of the specified tactic. /// @@ -93,4 +97,5 @@ public interface ITacticService /// The ID of the step to remove. /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the removal was successful. Task RemoveTacticStep(int tacticId, int stepId); + } \ No newline at end of file diff --git a/Services/ITeamService.cs b/Services/ITeamService.cs index d806fb6..bb2b1be 100644 --- a/Services/ITeamService.cs +++ b/Services/ITeamService.cs @@ -75,6 +75,11 @@ public interface ITeamService */ Authorized } + + public Task> GetSharedTacticsToTeam(int teamId); + + public Task IsUserInTeam(int userId, int teamId); + /** * Ensures that the given user identifier van perform an operation that requires the given role permission. @@ -82,4 +87,5 @@ public interface ITeamService * given team. */ public Task EnsureAccessibility(int userId, int teamId, MemberRole role); + } \ No newline at end of file diff --git a/Services/UserService.cs b/Services/UserService.cs index ca3de87..b81a507 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -46,6 +46,10 @@ public interface IUserService /// Updates an existing user. /// Task UpdateUser(User user); + + + public Task> GetSharedTacticsToUser(int userId); + /// /// Authorizes a user with the specified email and password. diff --git a/StubContext/StubAppContext.cs b/StubContext/StubAppContext.cs index 214800f..a53ba0a 100644 --- a/StubContext/StubAppContext.cs +++ b/StubContext/StubAppContext.cs @@ -53,6 +53,14 @@ public class StubAppContext(DbContextOptions options) : AppContext(o TacticId = 1, ParentId = null }); + + builder.Entity() + .HasData(new SharedTacticEntity + { + Id = 1, + TacticId = 1, + SharedWithUserId = 2 + }); builder.Entity() diff --git a/UnitTests/TeamsControllerTest.cs b/UnitTests/TeamsControllerTest.cs index 4484f9d..9bd32df 100644 --- a/UnitTests/TeamsControllerTest.cs +++ b/UnitTests/TeamsControllerTest.cs @@ -21,7 +21,7 @@ public class TeamsControllerTest ); context.Database.EnsureCreated(); var controller = new TeamsController( - new DbTeamService(context), + new DbTeamService(context), new DbTacticService(context), new ManualContextAccessor(userId) ); diff --git a/UnitTests/UserControllerTest.cs b/UnitTests/UserControllerTest.cs index 1fa04d2..03019cf 100644 --- a/UnitTests/UserControllerTest.cs +++ b/UnitTests/UserControllerTest.cs @@ -1,7 +1,7 @@ using API.Controllers; -using API.DTO; using DbServices; using FluentAssertions; +using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Model; @@ -50,4 +50,37 @@ public class UsersControllerTest // [new TacticDto(1, "New tactic", 1, "PLAIN", 1717106400000L)] // )); } + + [Fact] + public async Task ShareTacticTest() + { + var controller = GetUserController(1); + var result = await controller.ShareTactic(new UsersController.ShareTacticToUserRequest(1, 2)); + result.Should().BeOfType(); + } + + [Fact] + public async Task GetSharedTacticsToUserTest() + { + var controller = GetUserController(2); + var result = await controller.GetSharedTacticsToUser(2); + + var okResult = result as OkObjectResult; + var sharedTactics = okResult.Value as IEnumerable; + + sharedTactics.Should().NotBeNull(); + sharedTactics.Should().ContainSingle(); + + var tactic = sharedTactics.First(); + tactic.Id.Should().Be(1); + } + + [Fact] + public async Task UnshareTacticTest() + { + var controller = GetUserController(1); + var result = await controller.UnshareTactic(1, 2); + result.Should().BeOfType(); + } + } \ No newline at end of file