Files
mic-check/docs/api/06_admin_api.md
James Wampler 6887d09f9c version-0.1 (#1)
Squash and merge of version-0.1

Co-authored-by: James Wampler <james@wamp.dev>
Co-committed-by: James Wampler <james@wamp.dev>
2026-07-01 13:30:07 -07:00

326 lines
9.6 KiB
Markdown
Executable File

# Step 6: Admin API (Management Endpoints)
## Goal
Implement the Admin API — the authenticated management API for creating and managing projects, environments, features, segments, users, and API keys. Authenticated via `Authorization: Api-Key <TOKEN>` or JWT bearer token.
---
## API Versioning
```csharp
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
```
Routes:
- Current: `/api/v1/`
- Future: `/api/v2/`
All Admin API controllers:
```csharp
[ApiController]
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
[EnableRateLimiting("AdminApi")]
```
---
## Endpoint Groups
### Organizations
```
GET /api/v1/organisations/
POST /api/v1/organisations/
GET /api/v1/organisations/{id}/
PUT /api/v1/organisations/{id}/
DELETE /api/v1/organisations/{id}/
GET /api/v1/organisations/{id}/users/
POST /api/v1/organisations/{id}/users/invite/
DELETE /api/v1/organisations/{id}/users/{userId}/
```
### Projects
```
GET /api/v1/projects/
POST /api/v1/projects/
GET /api/v1/projects/{id}/
PUT /api/v1/projects/{id}/
DELETE /api/v1/projects/{id}/
GET /api/v1/projects/{id}/user-permissions/
POST /api/v1/projects/{id}/user-permissions/
PUT /api/v1/projects/{id}/user-permissions/{userId}/
DELETE /api/v1/projects/{id}/user-permissions/{userId}/
```
### Environments
```
GET /api/v1/environments/
POST /api/v1/environments/
GET /api/v1/environments/{apiKey}/
PUT /api/v1/environments/{apiKey}/
DELETE /api/v1/environments/{apiKey}/
POST /api/v1/environments/{apiKey}/clone/
GET /api/v1/environments/{apiKey}/webhooks/
POST /api/v1/environments/{apiKey}/webhooks/
PUT /api/v1/environments/{apiKey}/webhooks/{id}/
DELETE /api/v1/environments/{apiKey}/webhooks/{id}/
```
### Features
```
GET /api/v1/projects/{projectId}/features/
POST /api/v1/projects/{projectId}/features/
GET /api/v1/projects/{projectId}/features/{id}/
PUT /api/v1/projects/{projectId}/features/{id}/
PATCH /api/v1/projects/{projectId}/features/{id}/
DELETE /api/v1/projects/{projectId}/features/{id}/
```
### Feature States (Environment-level)
```
GET /api/v1/environments/{apiKey}/featurestates/
GET /api/v1/environments/{apiKey}/featurestates/{id}/
PUT /api/v1/environments/{apiKey}/featurestates/{id}/
PATCH /api/v1/environments/{apiKey}/featurestates/{id}/
```
### Segments
```
GET /api/v1/projects/{projectId}/segments/
POST /api/v1/projects/{projectId}/segments/
GET /api/v1/projects/{projectId}/segments/{id}/
PUT /api/v1/projects/{projectId}/segments/{id}/
DELETE /api/v1/projects/{projectId}/segments/{id}/
```
### Identities (Admin view)
```
GET /api/v1/environments/{apiKey}/identities/
GET /api/v1/environments/{apiKey}/identities/{id}/
DELETE /api/v1/environments/{apiKey}/identities/{id}/
GET /api/v1/environments/{apiKey}/identities/{id}/featurestates/
PUT /api/v1/environments/{apiKey}/identities/{id}/featurestates/{featureId}/
DELETE /api/v1/environments/{apiKey}/identities/{id}/featurestates/{featureId}/
```
### Audit Logs
```
GET /api/v1/organisations/{id}/audit-logs/
GET /api/v1/projects/{projectId}/audit-logs/
GET /api/v1/environments/{apiKey}/audit-logs/
```
### Tags
```
GET /api/v1/projects/{projectId}/tags/
POST /api/v1/projects/{projectId}/tags/
DELETE /api/v1/projects/{projectId}/tags/{id}/
```
---
## Service Layer
Each feature area has a service class in its folder within `MicCheck.Api`. Services accept and return domain objects — they have no knowledge of request/response DTOs. Services inject `MicCheckDbContext` directly.
```csharp
// MicCheck.Api/Features/FeatureService.cs
public class FeatureService
{
private readonly MicCheckDbContext _db;
private readonly AuditService _auditService;
// Enforces: max 400 features per project
// On create: auto-creates FeatureState records for all existing environments
public Task<Feature> CreateAsync(int projectId, string name, FeatureType type,
string? initialValue, string? description, CancellationToken ct = default);
public Task<Feature> UpdateAsync(int id, string name, string? description,
CancellationToken ct = default);
public Task DeleteAsync(int id, CancellationToken ct = default);
public Task<Feature?> FindByIdAsync(int id, CancellationToken ct = default);
public Task<IReadOnlyList<Feature>> ListByProjectAsync(int projectId,
CancellationToken ct = default);
}
// MicCheck.Api/Environments/EnvironmentService.cs
public class EnvironmentService
{
private readonly MicCheckDbContext _db;
private readonly AuditService _auditService;
// On create: auto-creates FeatureState records for all existing features
public Task<Environment> CreateAsync(int projectId, string name, CancellationToken ct = default);
// Clone: duplicates all environment-level FeatureStates; not identity/segment overrides
public Task<Environment> CloneAsync(string sourceApiKey, string newName, CancellationToken ct = default);
}
// MicCheck.Api/Segments/SegmentService.cs
public class SegmentService
{
private readonly MicCheckDbContext _db;
private readonly AuditService _auditService;
// Enforces: max 100 segments per project
// Enforces: max 100 segment rule conditions
public Task<Segment> CreateAsync(int projectId, string name,
IReadOnlyList<SegmentRuleDefinition> rules, CancellationToken ct = default);
}
```
---
## Controller Pattern
Controllers are responsible for:
1. Receiving the request DTO
2. Validating it (FluentValidation runs automatically via middleware)
3. Calling the appropriate service with extracted domain-level values
4. Mapping the returned domain object(s) to a response DTO
5. Returning the HTTP response
```csharp
// MicCheck.Api/Features/FeaturesController.cs
[ApiController]
[Route("api/v1/projects/{projectId}/features")]
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
[EnableRateLimiting("AdminApi")]
public class FeaturesController : ControllerBase
{
private readonly FeatureService _featureService;
[HttpPost]
public async Task<ActionResult<FeatureResponse>> Create(
int projectId, CreateFeatureRequest request, CancellationToken ct)
{
var feature = await _featureService.CreateAsync(
projectId, request.Name, request.Type, request.InitialValue, request.Description, ct);
return CreatedAtAction(nameof(GetById), new { projectId, id = feature.Id },
FeatureResponse.From(feature));
}
[HttpGet("{id}")]
public async Task<ActionResult<FeatureResponse>> GetById(int projectId, int id, CancellationToken ct)
{
var feature = await _featureService.FindByIdAsync(id, ct);
if (feature is null) return NotFound();
return Ok(FeatureResponse.From(feature));
}
}
```
---
## Request Validation
Use FluentValidation for all incoming request DTOs. Validators live in the same folder as the DTO they validate.
```csharp
// MicCheck.Api/Features/CreateFeatureRequest.cs
public record CreateFeatureRequest(
string Name,
FeatureType Type,
string? InitialValue,
string? Description
);
// MicCheck.Api/Features/CreateFeatureRequestValidator.cs
public class CreateFeatureRequestValidator : AbstractValidator<CreateFeatureRequest>
{
public CreateFeatureRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(150)
.Matches("^[a-zA-Z0-9_-]+$");
RuleFor(x => x.InitialValue)
.MaximumLength(20_000)
.When(x => x.InitialValue is not null);
}
}
```
---
## Response DTOs
Live in the same folder as their domain entity. The static `From` factory is the only mapping point.
```csharp
// MicCheck.Api/Features/FeatureResponse.cs
public record FeatureResponse(
int Id,
string Name,
string Type,
string? InitialValue,
string? Description,
bool DefaultEnabled,
DateTimeOffset CreatedAt
)
{
public static FeatureResponse From(Feature feature) => new(
feature.Id, feature.Name,
feature.Type.ToString().ToUpperInvariant(),
feature.InitialValue, feature.Description,
feature.DefaultEnabled, feature.CreatedAt);
}
```
Pagination wrapper for list endpoints:
```csharp
// MicCheck.Api/PaginatedResponse.cs
public record PaginatedResponse<T>(
int Count,
string? Next,
string? Previous,
IReadOnlyList<T> Results
);
```
---
## Environment Cloning
When cloning an environment:
1. Create new `Environment` record with a fresh `ApiKey`
2. Copy all environment-level `FeatureState` records from the source environment
3. Do NOT copy identity-level overrides
4. Do NOT copy segment overrides (those follow segment definitions)
5. Emit an audit log entry
---
## Acceptance Criteria
- [ ] Full CRUD for organizations, projects, environments, features, segments
- [ ] Creating a feature auto-creates `FeatureState` for all project environments
- [ ] Creating an environment auto-creates `FeatureState` for all project features
- [ ] 400 feature limit enforced (returns 400 with descriptive error)
- [ ] 100 segment limit enforced
- [ ] 100 segment condition limit enforced
- [ ] FluentValidation errors return 422 with field-level detail
- [ ] Environment clone creates independent copy of feature states
- [ ] All endpoints return 401 for missing credentials, 403 for insufficient permissions
- [ ] Paginated list endpoints default to page size 20, max 100
- [ ] Controllers never call `_db` directly — all data access goes through a service
- [ ] Services never reference request/response DTO types
- [ ] Unit tests for all service methods covering success paths and constraint violations