diff --git a/src/MicCheck.AppHost/MicCheck.AppHost.csproj b/src/MicCheck.AppHost/MicCheck.AppHost.csproj
index 6262cb9..41f4d69 100755
--- a/src/MicCheck.AppHost/MicCheck.AppHost.csproj
+++ b/src/MicCheck.AppHost/MicCheck.AppHost.csproj
@@ -14,6 +14,8 @@
net10.0
enable
enable
+ latest
+ true
d10a4485-2ac0-4ba7-bda5-8eb63e417567
diff --git a/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj b/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj
index 1efc067..bec9d25 100755
--- a/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj
+++ b/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj
@@ -4,6 +4,8 @@
net10.0
enable
enable
+ latest
+ true
true
diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKey.cs b/src/api/MicCheck.Api/ApiKeys/ApiKey.cs
deleted file mode 100755
index c3319e0..0000000
--- a/src/api/MicCheck.Api/ApiKeys/ApiKey.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace MicCheck.Api.ApiKeys;
-
-public class ApiKey
-{
- public int Id { get; init; }
- public required string Key { get; set; }
- public required string Prefix { get; set; }
- public required string Name { get; set; }
- public int OrganizationId { get; init; }
- public bool IsActive { get; set; }
- public DateTimeOffset? ExpiresAt { get; set; }
- public DateTimeOffset CreatedAt { get; init; }
-}
diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs
deleted file mode 100755
index ca100f9..0000000
--- a/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using MicCheck.Api.Authorization;
-
-namespace MicCheck.Api.ApiKeys;
-
-public static class ApiKeyEndpoints
-{
- public static void MapApiKeyEndpoints(this WebApplication app)
- {
- var group = app
- .MapGroup("/api/v1/organisations/{organizationId:int}/api-keys")
- .RequireAuthorization(AuthorizationPolicies.OrganizationAdmin)
- .WithTags("ApiKeys");
-
- group.MapPost("/", async (
- int organizationId,
- CreateApiKeyRequest request,
- ApiKeyService apiKeyService,
- CancellationToken ct) =>
- {
- var (key, rawKey) = await apiKeyService.CreateAsync(
- organizationId, request.Name, request.ExpiresAt, ct);
-
- return Results.Ok(new CreateApiKeyResponse(key.Id, key.Name, rawKey, key.Prefix, key.ExpiresAt));
- }).WithName("CreateApiKey");
-
- group.MapGet("/", async (
- int organizationId,
- ApiKeyService apiKeyService,
- CancellationToken ct) =>
- {
- var keys = await apiKeyService.ListAsync(organizationId, ct);
- return Results.Ok(keys.Select(k =>
- new ApiKeyResponse(k.Id, k.Name, k.Prefix, k.IsActive, k.ExpiresAt, k.CreatedAt)));
- }).WithName("ListApiKeys");
-
- group.MapDelete("/{keyId:int}", async (
- int organizationId,
- int keyId,
- ApiKeyService apiKeyService,
- CancellationToken ct) =>
- {
- await apiKeyService.RevokeAsync(organizationId, keyId, ct);
- return Results.NoContent();
- }).WithName("RevokeApiKey");
- }
-}
diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs
deleted file mode 100755
index 63a7445..0000000
--- a/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-
-namespace MicCheck.Api.ApiKeys;
-
-public static class ApiKeyHasher
-{
- public static string Hash(string key)
- {
- var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
- return Convert.ToHexString(bytes).ToLowerInvariant();
- }
-
- public static string GenerateKey()
- {
- var bytes = RandomNumberGenerator.GetBytes(32);
- return Convert.ToBase64String(bytes)
- .TrimEnd('=')
- .Replace('+', '-')
- .Replace('/', '_');
- }
-}
diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs
deleted file mode 100755
index 98e32d5..0000000
--- a/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MicCheck.Api.ApiKeys;
-
-public record ApiKeyResponse(
- int Id,
- string Name,
- string Prefix,
- bool IsActive,
- DateTimeOffset? ExpiresAt,
- DateTimeOffset CreatedAt);
diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs
deleted file mode 100755
index f697c5a..0000000
--- a/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using MicCheck.Api.Data;
-using Microsoft.EntityFrameworkCore;
-
-namespace MicCheck.Api.ApiKeys;
-
-public class ApiKeyService(MicCheckDbContext db)
-{
- public async Task<(ApiKey Key, string RawKey)> CreateAsync(
- int organizationId, string name, DateTimeOffset? expiresAt, CancellationToken ct = default)
- {
- var rawKey = ApiKeyHasher.GenerateKey();
- var hashedKey = ApiKeyHasher.Hash(rawKey);
- var prefix = rawKey.Length >= 8 ? rawKey[..8] : rawKey;
-
- var apiKey = new ApiKey
- {
- Key = hashedKey,
- Prefix = prefix,
- Name = name,
- OrganizationId = organizationId,
- IsActive = true,
- ExpiresAt = expiresAt,
- CreatedAt = DateTimeOffset.UtcNow
- };
-
- db.ApiKeys.Add(apiKey);
- await db.SaveChangesAsync(ct);
-
- return (apiKey, rawKey);
- }
-
- public async Task> ListAsync(int organizationId, CancellationToken ct = default)
- {
- return await db.ApiKeys
- .Where(k => k.OrganizationId == organizationId)
- .ToListAsync(ct);
- }
-
- public async Task RevokeAsync(int organizationId, int keyId, CancellationToken ct = default)
- {
- var key = await db.ApiKeys
- .FirstOrDefaultAsync(k => k.Id == keyId && k.OrganizationId == organizationId, ct);
-
- if (key is null)
- return;
-
- key.IsActive = false;
- await db.SaveChangesAsync(ct);
- }
-}
diff --git a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs b/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs
deleted file mode 100755
index f495617..0000000
--- a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.ApiKeys;
-
-public record CreateApiKeyRequest(string Name, DateTimeOffset? ExpiresAt);
diff --git a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs b/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs
deleted file mode 100755
index 615ede6..0000000
--- a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace MicCheck.Api.ApiKeys;
-
-public record CreateApiKeyResponse(
- int Id,
- string Name,
- string Key,
- string Prefix,
- DateTimeOffset? ExpiresAt);
diff --git a/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs b/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs
deleted file mode 100755
index 859bbe7..0000000
--- a/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System.Security.Claims;
-using System.Text.Encodings.Web;
-using MicCheck.Api.ApiKeys;
-using MicCheck.Api.Data;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Options;
-
-namespace MicCheck.Api.Authentication;
-
-public class ApiKeyAuthenticationHandler(
- IOptionsMonitor options,
- ILoggerFactory logger,
- UrlEncoder encoder,
- MicCheckDbContext db)
- : AuthenticationHandler(options, logger, encoder)
-{
- public const string SchemeName = "ApiKey";
- private const string ApiKeyPrefix = "Api-Key ";
-
- protected override async Task HandleAuthenticateAsync()
- {
- if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
- return AuthenticateResult.NoResult();
-
- var headerValue = authHeader.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(headerValue) ||
- !headerValue.StartsWith(ApiKeyPrefix, StringComparison.OrdinalIgnoreCase))
- return AuthenticateResult.NoResult();
-
- var rawKey = headerValue[ApiKeyPrefix.Length..].Trim();
- if (string.IsNullOrWhiteSpace(rawKey))
- return AuthenticateResult.Fail("API key value is empty.");
-
- var hashedKey = ApiKeyHasher.Hash(rawKey);
-
- var apiKey = await db.ApiKeys
- .FirstOrDefaultAsync(k => k.Key == hashedKey);
-
- if (apiKey is null)
- return AuthenticateResult.Fail("Invalid API key.");
-
- if (!apiKey.IsActive)
- return AuthenticateResult.Fail("API key is inactive.");
-
- if (apiKey.ExpiresAt.HasValue && apiKey.ExpiresAt.Value < DateTimeOffset.UtcNow)
- return AuthenticateResult.Fail("API key has expired.");
-
- var claims = new[]
- {
- new Claim("OrganizationId", apiKey.OrganizationId.ToString()),
- new Claim("OrganizationRole", "Admin")
- };
-
- var identity = new ClaimsIdentity(claims, Scheme.Name);
- var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
-
- return AuthenticateResult.Success(ticket);
- }
-}
diff --git a/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs b/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs
deleted file mode 100755
index 42cfe0f..0000000
--- a/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Security.Claims;
-using System.Text.Encodings.Web;
-using MicCheck.Api.Data;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Options;
-
-namespace MicCheck.Api.Authentication;
-
-public class EnvironmentKeyAuthenticationHandler(
- IOptionsMonitor options,
- ILoggerFactory logger,
- UrlEncoder encoder,
- MicCheckDbContext db)
- : AuthenticationHandler(options, logger, encoder)
-{
- public const string SchemeName = "EnvironmentKey";
-
- protected override async Task HandleAuthenticateAsync()
- {
- if (!Request.Headers.TryGetValue("X-Environment-Key", out var keyValues))
- return AuthenticateResult.NoResult();
-
- var key = keyValues.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(key))
- return AuthenticateResult.Fail("X-Environment-Key header is empty.");
-
- var environment = await db.Environments
- .FirstOrDefaultAsync(e => e.ApiKey == key);
-
- if (environment is null)
- return AuthenticateResult.Fail("Invalid environment key.");
-
- var claims = new[]
- {
- new Claim("EnvironmentId", environment.Id.ToString()),
- new Claim("ProjectId", environment.ProjectId.ToString())
- };
-
- var identity = new ClaimsIdentity(claims, Scheme.Name);
- var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
-
- return AuthenticateResult.Success(ticket);
- }
-}
diff --git a/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs b/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs
deleted file mode 100755
index 77b1477..0000000
--- a/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace MicCheck.Api.Authorization;
-
-public static class AuthEndpoints
-{
- public static void MapAuthEndpoints(this WebApplication app)
- {
- var group = app.MapGroup("/api/v1/auth").AllowAnonymous().WithTags("Auth");
-
- group.MapPost("/login", async (
- [FromBody] LoginRequest request,
- AuthService authService,
- CancellationToken ct) =>
- {
- if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
- return Results.BadRequest("Email and password are required.");
-
- var response = await authService.LoginAsync(request.Email, request.Password, ct);
- return response is null
- ? Results.Unauthorized()
- : Results.Ok(response);
- }).WithName("Login");
-
- group.MapPost("/register", async (
- [FromBody] RegisterRequest request,
- AuthService authService,
- CancellationToken ct) =>
- {
- if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
- return Results.BadRequest("Email and password are required.");
-
- var response = await authService.RegisterAsync(
- request.Email, request.Password,
- request.FirstName, request.LastName,
- request.OrganizationName, ct);
-
- return response is null
- ? Results.Conflict("An account with this email already exists.")
- : Results.Ok(response);
- }).WithName("Register");
-
- group.MapPost("/refresh", async (
- [FromBody] RefreshRequest request,
- AuthService authService,
- CancellationToken ct) =>
- {
- if (string.IsNullOrWhiteSpace(request.Token))
- return Results.BadRequest("Refresh token is required.");
-
- var response = await authService.RefreshAsync(request.Token, ct);
- return response is null
- ? Results.Unauthorized()
- : Results.Ok(response);
- }).WithName("RefreshToken");
-
- group.MapPost("/logout", async (
- [FromBody] LogoutRequest request,
- AuthService authService,
- CancellationToken ct) =>
- {
- await authService.LogoutAsync(request.Token, ct);
- return Results.NoContent();
- }).WithName("Logout");
- }
-}
diff --git a/src/api/MicCheck.Api/Authorization/AuthService.cs b/src/api/MicCheck.Api/Authorization/AuthService.cs
deleted file mode 100755
index 714ced0..0000000
--- a/src/api/MicCheck.Api/Authorization/AuthService.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-using System.Security.Cryptography;
-using MicCheck.Api.Data;
-using MicCheck.Api.Organizations;
-using MicCheck.Api.Users;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.EntityFrameworkCore;
-
-namespace MicCheck.Api.Authorization;
-
-public class AuthService(
- MicCheckDbContext db,
- ITokenService tokenService,
- IPasswordHasher passwordHasher)
-{
- public async Task LoginAsync(string email, string password, CancellationToken ct = default)
- {
- var user = await db.Users
- .Include(u => u.Organizations)
- .FirstOrDefaultAsync(u => u.Email == email && u.IsActive, ct);
-
- if (user is null)
- return null;
-
- var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
- if (result == PasswordVerificationResult.Failed)
- return null;
-
- var accessToken = tokenService.GenerateToken(user);
- var refreshTokenValue = GenerateSecureToken();
-
- db.RefreshTokens.Add(new RefreshToken
- {
- Token = refreshTokenValue,
- UserId = user.Id,
- ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
- CreatedAt = DateTimeOffset.UtcNow
- });
-
- user.LastLoginAt = DateTimeOffset.UtcNow;
- await db.SaveChangesAsync(ct);
-
- return new LoginResponse(accessToken.Token, refreshTokenValue, accessToken.ExpiresAt);
- }
-
- public async Task RegisterAsync(
- string email, string password,
- string firstName, string lastName,
- string organizationName,
- CancellationToken ct = default)
- {
- if (await db.Users.AnyAsync(u => u.Email == email, ct))
- return null;
-
- var user = new User
- {
- Email = email,
- FirstName = firstName,
- LastName = lastName,
- IsActive = true,
- CreatedAt = DateTimeOffset.UtcNow,
- PasswordHash = string.Empty
- };
- user.PasswordHash = passwordHasher.HashPassword(user, password);
-
- db.Users.Add(user);
- await db.SaveChangesAsync(ct);
-
- var organization = new Organization
- {
- Name = organizationName,
- CreatedAt = DateTimeOffset.UtcNow
- };
- db.Organizations.Add(organization);
- await db.SaveChangesAsync(ct);
-
- db.OrganizationUsers.Add(new OrganizationUser
- {
- OrganizationId = organization.Id,
- UserId = user.Id,
- Role = OrganizationRole.Admin
- });
- await db.SaveChangesAsync(ct);
-
- await db.Entry(user).Collection(u => u.Organizations).LoadAsync(ct);
-
- var accessToken = tokenService.GenerateToken(user);
- var refreshTokenValue = GenerateSecureToken();
-
- db.RefreshTokens.Add(new RefreshToken
- {
- Token = refreshTokenValue,
- UserId = user.Id,
- ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
- CreatedAt = DateTimeOffset.UtcNow
- });
- await db.SaveChangesAsync(ct);
-
- return new LoginResponse(accessToken.Token, refreshTokenValue, accessToken.ExpiresAt);
- }
-
- public async Task RefreshAsync(string refreshToken, CancellationToken ct = default)
- {
- var token = await db.RefreshTokens
- .Include(rt => rt.User)
- .ThenInclude(u => u.Organizations)
- .FirstOrDefaultAsync(rt => rt.Token == refreshToken && !rt.IsRevoked, ct);
-
- if (token is null || token.ExpiresAt < DateTimeOffset.UtcNow)
- return null;
-
- token.IsRevoked = true;
-
- var accessToken = tokenService.GenerateToken(token.User);
- var newRefreshTokenValue = GenerateSecureToken();
-
- db.RefreshTokens.Add(new RefreshToken
- {
- Token = newRefreshTokenValue,
- UserId = token.UserId,
- ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
- CreatedAt = DateTimeOffset.UtcNow
- });
- await db.SaveChangesAsync(ct);
-
- return new LoginResponse(accessToken.Token, newRefreshTokenValue, accessToken.ExpiresAt);
- }
-
- public async Task LogoutAsync(string refreshToken, CancellationToken ct = default)
- {
- var token = await db.RefreshTokens
- .FirstOrDefaultAsync(rt => rt.Token == refreshToken && !rt.IsRevoked, ct);
-
- if (token is null)
- return;
-
- token.IsRevoked = true;
- await db.SaveChangesAsync(ct);
- }
-
- private static string GenerateSecureToken()
- {
- var bytes = RandomNumberGenerator.GetBytes(64);
- return Convert.ToBase64String(bytes);
- }
-}
diff --git a/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs b/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs
deleted file mode 100755
index 005befd..0000000
--- a/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public static class AuthorizationPolicies
-{
- public const string FlagsApiAccess = "FlagsApiAccess";
- public const string AdminApiAccess = "AdminApiAccess";
- public const string OrganizationAdmin = "OrganizationAdmin";
-}
diff --git a/src/api/MicCheck.Api/Authorization/ITokenService.cs b/src/api/MicCheck.Api/Authorization/ITokenService.cs
deleted file mode 100755
index bbbfba1..0000000
--- a/src/api/MicCheck.Api/Authorization/ITokenService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using MicCheck.Api.Users;
-
-namespace MicCheck.Api.Authorization;
-
-public interface ITokenService
-{
- TokenResponse GenerateToken(User user);
-}
diff --git a/src/api/MicCheck.Api/Authorization/LoginRequest.cs b/src/api/MicCheck.Api/Authorization/LoginRequest.cs
deleted file mode 100755
index f1fbd85..0000000
--- a/src/api/MicCheck.Api/Authorization/LoginRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record LoginRequest(string Email, string Password);
diff --git a/src/api/MicCheck.Api/Authorization/LoginResponse.cs b/src/api/MicCheck.Api/Authorization/LoginResponse.cs
deleted file mode 100755
index e13a8d8..0000000
--- a/src/api/MicCheck.Api/Authorization/LoginResponse.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record LoginResponse(string AccessToken, string RefreshToken, DateTime ExpiresAt);
diff --git a/src/api/MicCheck.Api/Authorization/LogoutRequest.cs b/src/api/MicCheck.Api/Authorization/LogoutRequest.cs
deleted file mode 100755
index b379da0..0000000
--- a/src/api/MicCheck.Api/Authorization/LogoutRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record LogoutRequest(string Token);
diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermission.cs b/src/api/MicCheck.Api/Authorization/ProjectPermission.cs
deleted file mode 100755
index 83d795b..0000000
--- a/src/api/MicCheck.Api/Authorization/ProjectPermission.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public enum ProjectPermission
-{
- ViewProject,
- CreateFeature,
- EditFeature,
- DeleteFeature,
- CreateEnvironment,
- EditEnvironment,
- DeleteEnvironment,
- CreateSegment,
- EditSegment,
- DeleteSegment,
- ManageWebhooks,
- ViewAuditLog
-}
diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs b/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs
deleted file mode 100755
index af864db..0000000
--- a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace MicCheck.Api.Authorization;
-
-public class ProjectPermissionRequirement : IAuthorizationRequirement
-{
- public ProjectPermission Permission { get; }
-
- public ProjectPermissionRequirement(ProjectPermission permission) => Permission = permission;
-}
diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs b/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs
deleted file mode 100755
index c550931..0000000
--- a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.IdentityModel.Tokens.Jwt;
-using MicCheck.Api.Data;
-using MicCheck.Api.Organizations;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.EntityFrameworkCore;
-
-namespace MicCheck.Api.Authorization;
-
-public class ProjectPermissionRequirementHandler(
- MicCheckDbContext db,
- IHttpContextAccessor httpContextAccessor)
- : AuthorizationHandler
-{
- protected override async Task HandleRequirementAsync(
- AuthorizationHandlerContext context,
- ProjectPermissionRequirement requirement)
- {
- var userIdClaim = context.User.FindFirst(JwtRegisteredClaimNames.Sub);
- if (userIdClaim is null || !int.TryParse(userIdClaim.Value, out var userId))
- {
- context.Fail();
- return;
- }
-
- if (context.User.HasClaim("OrganizationRole", OrganizationRole.Admin.ToString()))
- {
- context.Succeed(requirement);
- return;
- }
-
- var httpContext = httpContextAccessor.HttpContext;
- if (httpContext is null ||
- !httpContext.Request.RouteValues.TryGetValue("projectId", out var projectIdValue) ||
- !int.TryParse(projectIdValue?.ToString(), out var projectId))
- {
- context.Fail();
- return;
- }
-
- var permission = await db.UserProjectPermissions
- .FirstOrDefaultAsync(p => p.UserId == userId && p.ProjectId == projectId);
-
- if (permission is null)
- {
- context.Fail();
- return;
- }
-
- if (permission.IsAdmin || permission.Permissions.Contains(requirement.Permission))
- context.Succeed(requirement);
- else
- context.Fail();
- }
-}
diff --git a/src/api/MicCheck.Api/Authorization/RefreshRequest.cs b/src/api/MicCheck.Api/Authorization/RefreshRequest.cs
deleted file mode 100755
index 94aa24e..0000000
--- a/src/api/MicCheck.Api/Authorization/RefreshRequest.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record RefreshRequest(string Token);
diff --git a/src/api/MicCheck.Api/Authorization/RegisterRequest.cs b/src/api/MicCheck.Api/Authorization/RegisterRequest.cs
deleted file mode 100755
index 58dcffe..0000000
--- a/src/api/MicCheck.Api/Authorization/RegisterRequest.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record RegisterRequest(
- string Email,
- string Password,
- string FirstName,
- string LastName,
- string OrganizationName);
diff --git a/src/api/MicCheck.Api/Authorization/TokenResponse.cs b/src/api/MicCheck.Api/Authorization/TokenResponse.cs
deleted file mode 100755
index 0439497..0000000
--- a/src/api/MicCheck.Api/Authorization/TokenResponse.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public record TokenResponse(string Token, DateTime ExpiresAt);
diff --git a/src/api/MicCheck.Api/Authorization/TokenService.cs b/src/api/MicCheck.Api/Authorization/TokenService.cs
deleted file mode 100755
index 089fb53..0000000
--- a/src/api/MicCheck.Api/Authorization/TokenService.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System.IdentityModel.Tokens.Jwt;
-using System.Security.Claims;
-using System.Text;
-using MicCheck.Api.Users;
-using Microsoft.IdentityModel.Tokens;
-
-namespace MicCheck.Api.Authorization;
-
-public class TokenService(IConfiguration configuration) : ITokenService
-{
- public TokenResponse GenerateToken(User user)
- {
- var secretKey = configuration["Jwt:SecretKey"]
- ?? throw new InvalidOperationException("Jwt:SecretKey is not configured.");
- var issuer = configuration["Jwt:Issuer"]
- ?? throw new InvalidOperationException("Jwt:Issuer is not configured.");
- var audience = configuration["Jwt:Audience"]
- ?? throw new InvalidOperationException("Jwt:Audience is not configured.");
- var expiryMinutes = int.Parse(configuration["Jwt:ExpiryMinutes"] ?? "60");
-
- var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
- var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
- var expiresAt = DateTime.UtcNow.AddMinutes(expiryMinutes);
-
- var claims = new List
- {
- new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
- new(JwtRegisteredClaimNames.Email, user.Email),
- new(JwtRegisteredClaimNames.GivenName, user.FirstName),
- new(JwtRegisteredClaimNames.FamilyName, user.LastName),
- new(JwtRegisteredClaimNames.Iat,
- DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
- ClaimValueTypes.Integer64),
- new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
- };
-
- foreach (var org in user.Organizations)
- {
- claims.Add(new Claim("OrganizationId", org.OrganizationId.ToString()));
- claims.Add(new Claim("OrganizationRole", org.Role.ToString()));
- }
-
- var token = new JwtSecurityToken(
- issuer: issuer,
- audience: audience,
- claims: claims,
- expires: expiresAt,
- signingCredentials: credentials);
-
- return new TokenResponse(new JwtSecurityTokenHandler().WriteToken(token), expiresAt);
- }
-}
diff --git a/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs b/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs
deleted file mode 100755
index 34001bd..0000000
--- a/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MicCheck.Api.Authorization;
-
-public class UserProjectPermission
-{
- public int UserId { get; init; }
- public int ProjectId { get; init; }
- public List Permissions { get; set; } = [];
- public bool IsAdmin { get; set; }
-}
diff --git a/src/api/MicCheck.Api/MicCheck.Api.csproj b/src/api/MicCheck.Api/MicCheck.Api.csproj
index b8e72e0..b650ca2 100755
--- a/src/api/MicCheck.Api/MicCheck.Api.csproj
+++ b/src/api/MicCheck.Api/MicCheck.Api.csproj
@@ -13,11 +13,12 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs
index f3d93b5..0a0c780 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs
@@ -1,4 +1,4 @@
-using MicCheck.Api.ApiKeys;
+using MicCheck.Api.Common.Security.ApiKeys;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.ApiKeys;
diff --git a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs
index 7993d7b..42c151d 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs
@@ -1,5 +1,5 @@
using NUnit.Framework;
-using MicCheck.Api.ApiKeys;
+using MicCheck.Api.Common.Security.ApiKeys;
namespace MicCheck.Api.Tests.Unit.ApiKeys;
diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs
index 021b1cd..e84fd79 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs
@@ -1,6 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
-using MicCheck.Api.ApiKeys;
+using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Data;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs
index 092c610..251437e 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs
@@ -1,6 +1,6 @@
using System.Net;
using System.Net.Http.Json;
-using MicCheck.Api.Authorization;
+using MicCheck.Api.Common.Security.Authorization;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs
index f331775..f2c323d 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs
@@ -1,6 +1,6 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
-using MicCheck.Api.Authorization;
+using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs
index 8235942..36fa883 100755
--- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs
+++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs
@@ -1,5 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
-using MicCheck.Api.Authorization;
+using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Organizations;
using MicCheck.Api.Users;
using Microsoft.Extensions.Configuration;