diff --git a/backend/LexBoxApi/GraphQL/LexMutations.cs b/backend/LexBoxApi/GraphQL/LexMutations.cs index 1fca95f13..f7343f96a 100644 --- a/backend/LexBoxApi/GraphQL/LexMutations.cs +++ b/backend/LexBoxApi/GraphQL/LexMutations.cs @@ -14,141 +14,4 @@ namespace LexBoxApi.GraphQL; [MutationType] public class LexMutations { - [Error] - [UseMutationConvention] - public async Task CreateProject( - LoggedInContext loggedInContext, - CreateProjectInput input, - [Service] ProjectService projectService, - LexBoxDbContext dbContext) - { - var projectId = await projectService.CreateProject(input, loggedInContext.User.Id); - return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == projectId); - } - - [Error] - [Error] - [UseMutationConvention] - public async Task AddProjectMember(AddProjectMemberInput input, - LexBoxDbContext dbContext) - { - var user = await dbContext.Users.FirstOrDefaultAsync(u => - u.Username == input.UserEmail || u.Email == input.UserEmail); - if (user is null) throw new NotFoundException("Member not found"); - dbContext.ProjectUsers.Add( - new ProjectUsers { Role = input.Role, ProjectId = input.ProjectId, UserId = user.Id }); - await dbContext.SaveChangesAsync(); - return await dbContext.Projects.Where(p => p.Id == input.ProjectId).FirstAsync(); - } - - [Error] - [Error] - [UseMutationConvention] - public async Task ChangeProjectMemberRole(ChangeProjectMemberRoleInput input, - LexBoxDbContext dbContext) - { - var projectUser = await dbContext.ProjectUsers.FirstOrDefaultAsync(u => u.ProjectId == input.ProjectId && u.UserId == input.UserId); - if (projectUser is null) throw new NotFoundException("Project member not found"); - projectUser.Role = input.Role; - await dbContext.SaveChangesAsync(); - return projectUser; - } - - [Error] - [Error] - [Error] - [UseMutationConvention] - public async Task ChangeProjectName(ChangeProjectNameInput input, - LexBoxDbContext dbContext) - { - if (input.Name.IsNullOrEmpty()) throw new RequiredException("Project name cannot be empty"); - - var project = await dbContext.Projects.FindAsync(input.ProjectId); - if (project is null) throw new NotFoundException("Project not found"); - - project.Name = input.Name; - await dbContext.SaveChangesAsync(); - return project; - } - - [Error] - [Error] - [UseMutationConvention] - public async Task ChangeProjectDescription(ChangeProjectDescriptionInput input, - LexBoxDbContext dbContext) - { - var project = await dbContext.Projects.FindAsync(input.ProjectId); - if (project is null) throw new NotFoundException("Project not found"); - - project.Description = input.Description; - await dbContext.SaveChangesAsync(); - return project; - } - - [UseFirstOrDefault] - [UseProjection] - public async Task> RemoveProjectMember(RemoveProjectMemberInput input, - LexBoxDbContext dbContext) - { - await dbContext.ProjectUsers.Where(pu => pu.ProjectId == input.ProjectId && pu.UserId == input.UserId) - .ExecuteDeleteAsync(); - return dbContext.Projects.Where(p => p.Id == input.ProjectId).AsExecutable(); - } - - [Error] - [Error] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [UseMutationConvention] - public async Task ChangeUserAccountData( - LoggedInContext loggedInContext, - ChangeUserAccountDataInput input, - LexBoxDbContext dbContext) - { - var user = await dbContext.Users.FindAsync(input.UserId); - if (user is null) throw new NotFoundException("User not found"); - if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException(); - // below works to change email - // minimum email = a@a.a - // if (input.Email is not null && input.Email != ""){ - // if (input.Email.Contains("@") == false || input.Email.Length < 3){ - // throw new RequiredException("Email does not match requirements"); - // } - // user.Email = input.Email; - // } - - if (!String.IsNullOrEmpty(input.Name)){ - user.Name = input.Name; - } - await dbContext.SaveChangesAsync(); - return user; - } - - [Error] - [Error] - [UseMutationConvention] - [AdminRequired] - public async Task ChangeUserAccountByAdmin(ChangeUserAccountByAdminInput input, LexBoxDbContext dbContext) - { - var user = await dbContext.Users.FindAsync(input.UserId); - if (user is null) throw new NotFoundException("User not found"); - if (!String.IsNullOrEmpty(input.Name)){ - user.Name = input.Name; - } - if (!String.IsNullOrEmpty(input.Email)){ - user.Email = input.Email; - } - await dbContext.SaveChangesAsync(); - return user; - } - - [Error] - [Error] - [UseMutationConvention] - [AdminRequired] - public async Task DeleteUserByAdmin(DeleteUserByAdminInput input, LexBoxDbContext dbContext){ - var User = await dbContext.Users.FindAsync(input.UserId); - var user = dbContext.Users.Where(u => u.Id == input.UserId); - await user.ExecuteDeleteAsync(); - return User; - } } diff --git a/backend/LexBoxApi/GraphQL/LexQueries.cs b/backend/LexBoxApi/GraphQL/LexQueries.cs index f9b4e623e..f63b4810c 100644 --- a/backend/LexBoxApi/GraphQL/LexQueries.cs +++ b/backend/LexBoxApi/GraphQL/LexQueries.cs @@ -30,8 +30,9 @@ public IQueryable Projects(LexBoxDbContext context) [UseSingleOrDefault] [UseProjection] - public IQueryable ProjectByCode(LexBoxDbContext context, string code) + public IQueryable ProjectByCode(LexBoxDbContext context, LoggedInContext loggedInContext, string code) { + loggedInContext.User.AssertCanAccessProject(code); return context.Projects.Where(p => p.Code == code); } diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs new file mode 100644 index 000000000..f6baf1263 --- /dev/null +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -0,0 +1,106 @@ +using LexBoxApi.Auth; +using LexBoxApi.Models.Project; +using LexBoxApi.Services; +using LexCore.Entities; +using LexCore.Exceptions; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; + +namespace LexBoxApi.GraphQL; + +[MutationType] +public class ProjectMutations +{ + [Error] + [UseMutationConvention] + public async Task CreateProject( + LoggedInContext loggedInContext, + CreateProjectInput input, + [Service] ProjectService projectService, + LexBoxDbContext dbContext) + { + var projectId = await projectService.CreateProject(input, loggedInContext.User.Id); + return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == projectId); + } + + [Error] + [Error] + [UseMutationConvention] + public async Task AddProjectMember(LoggedInContext loggedInContext, AddProjectMemberInput input, LexBoxDbContext dbContext) + { + loggedInContext.User.AssertCanManageProject(input.ProjectId); + var user = await dbContext.Users.FirstOrDefaultAsync(u => + u.Username == input.UserEmail || u.Email == input.UserEmail); + if (user is null) throw new NotFoundException("Member not found"); + dbContext.ProjectUsers.Add( + new ProjectUsers { Role = input.Role, ProjectId = input.ProjectId, UserId = user.Id }); + await dbContext.SaveChangesAsync(); + return await dbContext.Projects.Where(p => p.Id == input.ProjectId).FirstAsync(); + } + + [Error] + [Error] + [UseMutationConvention] + public async Task ChangeProjectMemberRole( + ChangeProjectMemberRoleInput input, + LoggedInContext loggedInContext, + LexBoxDbContext dbContext) + { + loggedInContext.User.AssertCanManageProject(input.ProjectId); + var projectUser = + await dbContext.ProjectUsers.FirstOrDefaultAsync(u => + u.ProjectId == input.ProjectId && u.UserId == input.UserId); + if (projectUser is null) throw new NotFoundException("Project member not found"); + projectUser.Role = input.Role; + await dbContext.SaveChangesAsync(); + return projectUser; + } + + [Error] + [Error] + [Error] + [UseMutationConvention] + public async Task ChangeProjectName(ChangeProjectNameInput input, + LoggedInContext loggedInContext, + LexBoxDbContext dbContext) + { + loggedInContext.User.AssertCanManageProject(input.ProjectId); + if (input.Name.IsNullOrEmpty()) throw new RequiredException("Project name cannot be empty"); + + var project = await dbContext.Projects.FindAsync(input.ProjectId); + if (project is null) throw new NotFoundException("Project not found"); + + project.Name = input.Name; + await dbContext.SaveChangesAsync(); + return project; + } + + [Error] + [Error] + [UseMutationConvention] + public async Task ChangeProjectDescription(ChangeProjectDescriptionInput input, + LoggedInContext loggedInContext, + LexBoxDbContext dbContext) + { + loggedInContext.User.AssertCanManageProject(input.ProjectId); + var project = await dbContext.Projects.FindAsync(input.ProjectId); + if (project is null) throw new NotFoundException("Project not found"); + + project.Description = input.Description; + await dbContext.SaveChangesAsync(); + return project; + } + + [UseFirstOrDefault] + [UseProjection] + public async Task> RemoveProjectMember(RemoveProjectMemberInput input, + LoggedInContext loggedInContext, + LexBoxDbContext dbContext) + { + loggedInContext.User.AssertCanManageProject(input.ProjectId); + await dbContext.ProjectUsers.Where(pu => pu.ProjectId == input.ProjectId && pu.UserId == input.UserId) + .ExecuteDeleteAsync(); + return dbContext.Projects.Where(p => p.Id == input.ProjectId).AsExecutable(); + } +} diff --git a/backend/LexBoxApi/GraphQL/UserMutations.cs b/backend/LexBoxApi/GraphQL/UserMutations.cs new file mode 100644 index 000000000..4a240ff3f --- /dev/null +++ b/backend/LexBoxApi/GraphQL/UserMutations.cs @@ -0,0 +1,77 @@ +using LexBoxApi.Auth; +using LexBoxApi.Models.Project; +using LexCore.Entities; +using LexCore.Exceptions; +using LexData; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LexBoxApi.GraphQL; + +[MutationType] +public class UserMutations +{ + [Error] + [Error] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [UseMutationConvention] + public async Task ChangeUserAccountData( + LoggedInContext loggedInContext, + ChangeUserAccountDataInput input, + LexBoxDbContext dbContext) + { + if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException(); + var user = await dbContext.Users.FindAsync(input.UserId); + if (user is null) throw new NotFoundException("User not found"); + // below works to change email + // minimum email = a@a.a + // if (input.Email is not null && input.Email != ""){ + // if (input.Email.Contains("@") == false || input.Email.Length < 3){ + // throw new RequiredException("Email does not match requirements"); + // } + // user.Email = input.Email; + // } + + if (!String.IsNullOrEmpty(input.Name)) + { + user.Name = input.Name; + } + + await dbContext.SaveChangesAsync(); + return user; + } + + [Error] + [Error] + [UseMutationConvention] + [AdminRequired] + public async Task ChangeUserAccountByAdmin(ChangeUserAccountByAdminInput input, LexBoxDbContext dbContext) + { + var user = await dbContext.Users.FindAsync(input.UserId); + if (user is null) throw new NotFoundException("User not found"); + if (!String.IsNullOrEmpty(input.Name)) + { + user.Name = input.Name; + } + + if (!String.IsNullOrEmpty(input.Email)) + { + user.Email = input.Email; + } + + await dbContext.SaveChangesAsync(); + return user; + } + + [Error] + [Error] + [UseMutationConvention] + [AdminRequired] + public async Task DeleteUserByAdmin(DeleteUserByAdminInput input, LexBoxDbContext dbContext) + { + var User = await dbContext.Users.FindAsync(input.UserId); + var user = dbContext.Users.Where(u => u.Id == input.UserId); + await user.ExecuteDeleteAsync(); + return User; + } +} diff --git a/backend/LexCore/Auth/LexAuthUser.cs b/backend/LexCore/Auth/LexAuthUser.cs index 671b8c639..70677ab18 100644 --- a/backend/LexCore/Auth/LexAuthUser.cs +++ b/backend/LexCore/Auth/LexAuthUser.cs @@ -67,7 +67,7 @@ public LexAuthUser(User user) Email = user.Email; Role = user.IsAdmin ? UserRole.admin : UserRole.user; Name = user.Name; - Projects = user.Projects.Select(p => new AuthUserProject(p.Project.Code, p.Role)).ToArray(); + Projects = user.Projects.Select(p => new AuthUserProject(p.Project.Code, p.Role, p.ProjectId)).ToArray(); } [JsonPropertyName(LexAuthConstants.IdClaimType)] @@ -117,9 +117,29 @@ public ClaimsPrincipal GetPrincipal(string authenticationType) LexAuthConstants.EmailClaimType, LexAuthConstants.RoleClaimType)); } + + public bool CanManageProject(Guid projectId) + { + return Role == UserRole.admin || Projects.Any(p => p.ProjectId == projectId && p.Role == ProjectRole.Manager); + } + + public bool CanManageProject(string projectCode) + { + return Role == UserRole.admin || Projects.Any(p => p.Code == projectCode && p.Role == ProjectRole.Manager); + } + + public void AssertCanManageProject(Guid projectId) + { + if (!CanManageProject(projectId)) throw new UnauthorizedAccessException(); + } + + public void AssertCanAccessProject(string projectCode) + { + if (Role != UserRole.admin && Projects.All(p => p.Code != projectCode)) throw new UnauthorizedAccessException(); + } } -public record AuthUserProject(string Code, ProjectRole Role); +public record AuthUserProject(string Code, ProjectRole Role, Guid ProjectId); [JsonConverter(typeof(JsonStringEnumConverter))] public enum UserRole diff --git a/backend/Testing/LexCore/LexAuthUserTests.cs b/backend/Testing/LexCore/LexAuthUserTests.cs index 698912dfd..32a6b77ae 100644 --- a/backend/Testing/LexCore/LexAuthUserTests.cs +++ b/backend/Testing/LexCore/LexAuthUserTests.cs @@ -22,7 +22,7 @@ public class LexAuthUserTests Name = "test", Projects = new[] { - new AuthUserProject("test-flex", ProjectRole.Manager) + new AuthUserProject("test-flex", ProjectRole.Manager, Guid.NewGuid()) } }; diff --git a/frontend/schema.graphql b/frontend/schema.graphql index c636e7d22..8af2ce18d 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -36,6 +36,7 @@ enum ApplyPolicy { type AuthUserProject { code: String! + projectId: UUID! role: ProjectRole! } @@ -181,6 +182,7 @@ type IsAdminResponse { } type LexAuthUser { + canManageProject(projectId: UUID!): Boolean! email: String! id: UUID! name: String!