This commit is contained in:
2026-06-10 13:06:55 -07:00
parent 4621e8380b
commit 570ab363c2
987 changed files with 22413 additions and 21031 deletions

View File

0
src/api/MicCheck.Api/Projects/CreateProjectRequest.cs Normal file → Executable file
View File

0
src/api/MicCheck.Api/Projects/Project.cs Normal file → Executable file
View File

54
src/api/MicCheck.Api/Projects/ProjectResponse.cs Normal file → Executable file
View File

@@ -1,27 +1,27 @@
using MicCheck.Api.Common.Security.Authorization;
namespace MicCheck.Api.Projects;
public record ProjectResponse(
int Id,
string Name,
int OrganizationId,
bool HideDisabledFlags,
DateTimeOffset CreatedAt
)
{
public static ProjectResponse From(Project project) => new(
project.Id, project.Name, project.OrganizationId, project.HideDisabledFlags, project.CreatedAt);
}
public record UserPermissionResponse(
int UserId,
int ProjectId,
bool IsAdmin,
IReadOnlyList<string> Permissions
)
{
public static UserPermissionResponse From(UserProjectPermission p) => new(
p.UserId, p.ProjectId, p.IsAdmin,
p.Permissions.Select(x => x.ToString()).ToList());
}
using MicCheck.Api.Common.Security.Authorization;
namespace MicCheck.Api.Projects;
public record ProjectResponse(
int Id,
string Name,
int OrganizationId,
bool HideDisabledFlags,
DateTimeOffset CreatedAt
)
{
public static ProjectResponse From(Project project) => new(
project.Id, project.Name, project.OrganizationId, project.HideDisabledFlags, project.CreatedAt);
}
public record UserPermissionResponse(
int UserId,
int ProjectId,
bool IsAdmin,
IReadOnlyList<string> Permissions
)
{
public static UserPermissionResponse From(UserProjectPermission p) => new(
p.UserId, p.ProjectId, p.IsAdmin,
p.Permissions.Select(x => x.ToString()).ToList());
}

210
src/api/MicCheck.Api/Projects/ProjectService.cs Normal file → Executable file
View File

@@ -1,105 +1,105 @@
using MicCheck.Api.Audit;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Data;
using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Projects;
public class ProjectService(MicCheckDbContext db, AuditService auditService)
{
public async Task<IReadOnlyList<Project>> ListByOrganizationAsync(int organizationId, CancellationToken ct = default)
{
return await db.Projects
.Where(p => p.OrganizationId == organizationId)
.ToListAsync(ct);
}
public async Task<Project?> FindByIdAsync(int id, CancellationToken ct = default)
{
return await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct);
}
public async Task<Project> CreateAsync(int organizationId, string name, CancellationToken ct = default)
{
var project = new Project
{
Name = name,
OrganizationId = organizationId,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
await db.SaveChangesAsync(ct);
await auditService.LogAsync("Project", project.Id.ToString(), "created", organizationId, project.Id, ct: ct);
return project;
}
public async Task<Project> UpdateAsync(int id, string name, bool hideDisabledFlags, CancellationToken ct = default)
{
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct)
?? throw new KeyNotFoundException($"Project {id} not found.");
project.Name = name;
project.HideDisabledFlags = hideDisabledFlags;
await db.SaveChangesAsync(ct);
await auditService.LogAsync("Project", project.Id.ToString(), "updated", project.OrganizationId, project.Id, ct: ct);
return project;
}
public async Task DeleteAsync(int id, CancellationToken ct = default)
{
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct);
if (project is null) return;
db.Projects.Remove(project);
await db.SaveChangesAsync(ct);
}
public async Task<IReadOnlyList<UserProjectPermission>> ListUserPermissionsAsync(int projectId, CancellationToken ct = default)
{
return await db.UserProjectPermissions
.Where(p => p.ProjectId == projectId)
.ToListAsync(ct);
}
public async Task<UserProjectPermission> SetUserPermissionsAsync(
int projectId, int userId, bool isAdmin, List<ProjectPermission> permissions, CancellationToken ct = default)
{
var existing = await db.UserProjectPermissions
.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId, ct);
if (existing is not null)
{
existing.IsAdmin = isAdmin;
existing.Permissions = permissions;
}
else
{
existing = new UserProjectPermission
{
ProjectId = projectId,
UserId = userId,
IsAdmin = isAdmin,
Permissions = permissions
};
db.UserProjectPermissions.Add(existing);
}
await db.SaveChangesAsync(ct);
return existing;
}
public async Task RemoveUserPermissionsAsync(int projectId, int userId, CancellationToken ct = default)
{
var perm = await db.UserProjectPermissions
.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId, ct);
if (perm is null) return;
db.UserProjectPermissions.Remove(perm);
await db.SaveChangesAsync(ct);
}
}
using MicCheck.Api.Audit;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Data;
using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Projects;
public class ProjectService(MicCheckDbContext db, AuditService auditService)
{
public async Task<IReadOnlyList<Project>> ListByOrganizationAsync(int organizationId, CancellationToken ct = default)
{
return await db.Projects
.Where(p => p.OrganizationId == organizationId)
.ToListAsync(ct);
}
public async Task<Project?> FindByIdAsync(int id, CancellationToken ct = default)
{
return await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct);
}
public async Task<Project> CreateAsync(int organizationId, string name, CancellationToken ct = default)
{
var project = new Project
{
Name = name,
OrganizationId = organizationId,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
await db.SaveChangesAsync(ct);
await auditService.LogAsync("Project", project.Id.ToString(), "created", organizationId, project.Id, ct: ct);
return project;
}
public async Task<Project> UpdateAsync(int id, string name, bool hideDisabledFlags, CancellationToken ct = default)
{
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct)
?? throw new KeyNotFoundException($"Project {id} not found.");
project.Name = name;
project.HideDisabledFlags = hideDisabledFlags;
await db.SaveChangesAsync(ct);
await auditService.LogAsync("Project", project.Id.ToString(), "updated", project.OrganizationId, project.Id, ct: ct);
return project;
}
public async Task DeleteAsync(int id, CancellationToken ct = default)
{
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct);
if (project is null) return;
db.Projects.Remove(project);
await db.SaveChangesAsync(ct);
}
public async Task<IReadOnlyList<UserProjectPermission>> ListUserPermissionsAsync(int projectId, CancellationToken ct = default)
{
return await db.UserProjectPermissions
.Where(p => p.ProjectId == projectId)
.ToListAsync(ct);
}
public async Task<UserProjectPermission> SetUserPermissionsAsync(
int projectId, int userId, bool isAdmin, List<ProjectPermission> permissions, CancellationToken ct = default)
{
var existing = await db.UserProjectPermissions
.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId, ct);
if (existing is not null)
{
existing.IsAdmin = isAdmin;
existing.Permissions = permissions;
}
else
{
existing = new UserProjectPermission
{
ProjectId = projectId,
UserId = userId,
IsAdmin = isAdmin,
Permissions = permissions
};
db.UserProjectPermissions.Add(existing);
}
await db.SaveChangesAsync(ct);
return existing;
}
public async Task RemoveUserPermissionsAsync(int projectId, int userId, CancellationToken ct = default)
{
var perm = await db.UserProjectPermissions
.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId, ct);
if (perm is null) return;
db.UserProjectPermissions.Remove(perm);
await db.SaveChangesAsync(ct);
}
}

226
src/api/MicCheck.Api/Projects/ProjectsController.cs Normal file → Executable file
View File

@@ -1,113 +1,113 @@
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace MicCheck.Api.Projects;
[ApiController]
[Route("api/v1/projects")]
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
[EnableRateLimiting("AdminApi")]
public class ProjectsController(ProjectService projectService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PaginatedResponse<ProjectResponse>>> List(
[FromQuery] int organizationId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken ct = default)
{
pageSize = Math.Clamp(pageSize, 1, 100);
var all = await projectService.ListByOrganizationAsync(organizationId, ct);
var paged = all.Skip((page - 1) * pageSize).Take(pageSize).Select(ProjectResponse.From).ToList();
return Ok(new PaginatedResponse<ProjectResponse>(all.Count, null, null, paged));
}
[HttpPost]
public async Task<ActionResult<ProjectResponse>> Create(CreateProjectRequest request, CancellationToken ct)
{
var project = await projectService.CreateAsync(request.OrganizationId, request.Name, ct);
return CreatedAtAction(nameof(GetById), new { id = project.Id }, ProjectResponse.From(project));
}
[HttpGet("{id}")]
public async Task<ActionResult<ProjectResponse>> GetById(int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
return Ok(ProjectResponse.From(project));
}
[HttpPut("{id}")]
public async Task<ActionResult<ProjectResponse>> Update(int id, UpdateProjectRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var updated = await projectService.UpdateAsync(id, request.Name, request.HideDisabledFlags, ct);
return Ok(ProjectResponse.From(updated));
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
await projectService.DeleteAsync(id, ct);
return NoContent();
}
[HttpGet("{id}/user-permissions")]
public async Task<ActionResult<IReadOnlyList<UserPermissionResponse>>> ListUserPermissions(
int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var perms = await projectService.ListUserPermissionsAsync(id, ct);
return Ok(perms.Select(UserPermissionResponse.From).ToList());
}
[HttpPost("{id}/user-permissions")]
public async Task<ActionResult<UserPermissionResponse>> CreateUserPermissions(
int id, SetUserPermissionsRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var permissions = request.Permissions
.Select(p => Enum.Parse<ProjectPermission>(p, ignoreCase: true))
.ToList();
var perm = await projectService.SetUserPermissionsAsync(id, request.UserId, request.IsAdmin, permissions, ct);
return Ok(UserPermissionResponse.From(perm));
}
[HttpPut("{id}/user-permissions/{userId}")]
public async Task<ActionResult<UserPermissionResponse>> UpdateUserPermissions(
int id, int userId, SetUserPermissionsRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var permissions = request.Permissions
.Select(p => Enum.Parse<ProjectPermission>(p, ignoreCase: true))
.ToList();
var perm = await projectService.SetUserPermissionsAsync(id, userId, request.IsAdmin, permissions, ct);
return Ok(UserPermissionResponse.From(perm));
}
[HttpDelete("{id}/user-permissions/{userId}")]
public async Task<IActionResult> DeleteUserPermissions(int id, int userId, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
await projectService.RemoveUserPermissionsAsync(id, userId, ct);
return NoContent();
}
}
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace MicCheck.Api.Projects;
[ApiController]
[Route("api/v1/projects")]
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
[EnableRateLimiting("AdminApi")]
public class ProjectsController(ProjectService projectService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PaginatedResponse<ProjectResponse>>> List(
[FromQuery] int organizationId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken ct = default)
{
pageSize = Math.Clamp(pageSize, 1, 100);
var all = await projectService.ListByOrganizationAsync(organizationId, ct);
var paged = all.Skip((page - 1) * pageSize).Take(pageSize).Select(ProjectResponse.From).ToList();
return Ok(new PaginatedResponse<ProjectResponse>(all.Count, null, null, paged));
}
[HttpPost]
public async Task<ActionResult<ProjectResponse>> Create(CreateProjectRequest request, CancellationToken ct)
{
var project = await projectService.CreateAsync(request.OrganizationId, request.Name, ct);
return CreatedAtAction(nameof(GetById), new { id = project.Id }, ProjectResponse.From(project));
}
[HttpGet("{id}")]
public async Task<ActionResult<ProjectResponse>> GetById(int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
return Ok(ProjectResponse.From(project));
}
[HttpPut("{id}")]
public async Task<ActionResult<ProjectResponse>> Update(int id, UpdateProjectRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var updated = await projectService.UpdateAsync(id, request.Name, request.HideDisabledFlags, ct);
return Ok(ProjectResponse.From(updated));
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
await projectService.DeleteAsync(id, ct);
return NoContent();
}
[HttpGet("{id}/user-permissions")]
public async Task<ActionResult<IReadOnlyList<UserPermissionResponse>>> ListUserPermissions(
int id, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var perms = await projectService.ListUserPermissionsAsync(id, ct);
return Ok(perms.Select(UserPermissionResponse.From).ToList());
}
[HttpPost("{id}/user-permissions")]
public async Task<ActionResult<UserPermissionResponse>> CreateUserPermissions(
int id, SetUserPermissionsRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var permissions = request.Permissions
.Select(p => Enum.Parse<ProjectPermission>(p, ignoreCase: true))
.ToList();
var perm = await projectService.SetUserPermissionsAsync(id, request.UserId, request.IsAdmin, permissions, ct);
return Ok(UserPermissionResponse.From(perm));
}
[HttpPut("{id}/user-permissions/{userId}")]
public async Task<ActionResult<UserPermissionResponse>> UpdateUserPermissions(
int id, int userId, SetUserPermissionsRequest request, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
var permissions = request.Permissions
.Select(p => Enum.Parse<ProjectPermission>(p, ignoreCase: true))
.ToList();
var perm = await projectService.SetUserPermissionsAsync(id, userId, request.IsAdmin, permissions, ct);
return Ok(UserPermissionResponse.From(perm));
}
[HttpDelete("{id}/user-permissions/{userId}")]
public async Task<IActionResult> DeleteUserPermissions(int id, int userId, CancellationToken ct)
{
var project = await projectService.FindByIdAsync(id, ct);
if (project is null) return NotFound();
await projectService.RemoveUserPermissionsAsync(id, userId, ct);
return NoContent();
}
}

View File

@@ -1,17 +1,17 @@
using FluentValidation;
using MicCheck.Api.Common.Security.Authorization;
namespace MicCheck.Api.Projects;
public record SetUserPermissionsRequest(int UserId, bool IsAdmin, List<string> Permissions);
public class SetUserPermissionsRequestValidator : AbstractValidator<SetUserPermissionsRequest>
{
public SetUserPermissionsRequestValidator()
{
RuleFor(x => x.UserId).GreaterThan(0);
RuleForEach(x => x.Permissions)
.Must(p => Enum.TryParse<ProjectPermission>(p, true, out _))
.WithMessage("Invalid permission value.");
}
}
using FluentValidation;
using MicCheck.Api.Common.Security.Authorization;
namespace MicCheck.Api.Projects;
public record SetUserPermissionsRequest(int UserId, bool IsAdmin, List<string> Permissions);
public class SetUserPermissionsRequestValidator : AbstractValidator<SetUserPermissionsRequest>
{
public SetUserPermissionsRequestValidator()
{
RuleFor(x => x.UserId).GreaterThan(0);
RuleForEach(x => x.Permissions)
.Must(p => Enum.TryParse<ProjectPermission>(p, true, out _))
.WithMessage("Invalid permission value.");
}
}

0
src/api/MicCheck.Api/Projects/UpdateProjectRequest.cs Normal file → Executable file
View File