90ac0b81d1
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
126 lines
5.4 KiB
C#
126 lines
5.4 KiB
C#
using FlatRender.IdentitySvc.Application.Services.Interfaces;
|
|
using FlatRender.IdentitySvc.Models.Requests;
|
|
using FlatRender.IdentitySvc.Models.Responses;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace FlatRender.IdentitySvc.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("v1/tenants")]
|
|
[Authorize]
|
|
public class TenantsController(ITenantService tenantService) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
[ProducesResponseType(typeof(PagedResponse<TenantResponse>), 200)]
|
|
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
|
=> Ok(await tenantService.ListAsync(page, pageSize));
|
|
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(TenantResponse), 201)]
|
|
public async Task<IActionResult> Create([FromBody] CreateTenantRequest request)
|
|
{
|
|
var result = await tenantService.CreateAsync(request);
|
|
return StatusCode(201, result);
|
|
}
|
|
|
|
[HttpGet("by-slug/{slug}")]
|
|
[AllowAnonymous]
|
|
[ProducesResponseType(typeof(TenantResponse), 200)]
|
|
public async Task<IActionResult> GetBySlug(string slug)
|
|
=> Ok(await tenantService.GetBySlugAsync(slug));
|
|
|
|
[HttpGet("{tenantId:guid}")]
|
|
[ProducesResponseType(typeof(TenantResponse), 200)]
|
|
public async Task<IActionResult> GetById(Guid tenantId)
|
|
=> Ok(await tenantService.GetByIdAsync(tenantId));
|
|
|
|
[HttpPatch("{tenantId:guid}")]
|
|
[ProducesResponseType(typeof(TenantResponse), 200)]
|
|
public async Task<IActionResult> Update(Guid tenantId, [FromBody] UpdateTenantRequest request)
|
|
=> Ok(await tenantService.UpdateAsync(tenantId, request));
|
|
|
|
[HttpGet("{tenantId:guid}/branding")]
|
|
[ProducesResponseType(typeof(TenantBrandingResponse), 200)]
|
|
public async Task<IActionResult> GetBranding(Guid tenantId)
|
|
=> Ok(await tenantService.GetBrandingAsync(tenantId));
|
|
|
|
[HttpPut("{tenantId:guid}/branding")]
|
|
[ProducesResponseType(typeof(TenantBrandingResponse), 200)]
|
|
public async Task<IActionResult> UpsertBranding(Guid tenantId, [FromBody] TenantBrandingRequest request)
|
|
=> Ok(await tenantService.UpsertBrandingAsync(tenantId, request));
|
|
|
|
[HttpPost("{tenantId:guid}/domains/verify")]
|
|
[ProducesResponseType(typeof(DomainVerificationResponse), 200)]
|
|
public async Task<IActionResult> VerifyDomain(Guid tenantId, [FromBody] StartDomainVerificationRequest request)
|
|
=> Ok(await tenantService.StartDomainVerificationAsync(tenantId, request.Domain, request.Method));
|
|
|
|
[HttpGet("{tenantId:guid}/usage")]
|
|
[ProducesResponseType(typeof(object), 200)]
|
|
public async Task<IActionResult> GetUsage(
|
|
Guid tenantId,
|
|
[FromQuery] DateOnly from,
|
|
[FromQuery] DateOnly to)
|
|
{
|
|
var data = await tenantService.GetUsageAsync(tenantId, from, to);
|
|
return Ok(new { data });
|
|
}
|
|
|
|
// ── API Keys ──────────────────────────────────────────────────────────
|
|
|
|
[HttpGet("{tenantId:guid}/api-keys")]
|
|
public async Task<IActionResult> GetApiKeys(Guid tenantId)
|
|
=> Ok(new { data = await tenantService.GetApiKeysAsync(tenantId) });
|
|
|
|
[HttpPost("{tenantId:guid}/api-keys")]
|
|
public async Task<IActionResult> CreateApiKey(Guid tenantId, [FromBody] CreateApiKeyRequest request)
|
|
{
|
|
var userId = GetUserId();
|
|
var result = await tenantService.CreateApiKeyAsync(tenantId, userId, request);
|
|
return StatusCode(201, result);
|
|
}
|
|
|
|
[HttpDelete("{tenantId:guid}/api-keys/{apiKeyId:guid}")]
|
|
public async Task<IActionResult> RevokeApiKey(Guid tenantId, Guid apiKeyId, [FromBody] RevokeApiKeyRequest? request)
|
|
{
|
|
await tenantService.RevokeApiKeyAsync(tenantId, apiKeyId, request?.Reason);
|
|
return NoContent();
|
|
}
|
|
|
|
// ── Webhooks ──────────────────────────────────────────────────────────
|
|
|
|
[HttpGet("{tenantId:guid}/webhooks")]
|
|
public async Task<IActionResult> GetWebhooks(Guid tenantId)
|
|
=> Ok(new { data = await tenantService.GetWebhooksAsync(tenantId) });
|
|
|
|
[HttpPost("{tenantId:guid}/webhooks")]
|
|
public async Task<IActionResult> CreateWebhook(Guid tenantId, [FromBody] CreateWebhookRequest request)
|
|
{
|
|
var result = await tenantService.CreateWebhookAsync(tenantId, request);
|
|
return StatusCode(201, result);
|
|
}
|
|
|
|
[HttpDelete("{tenantId:guid}/webhooks/{webhookId:guid}")]
|
|
public async Task<IActionResult> DeleteWebhook(Guid tenantId, Guid webhookId)
|
|
{
|
|
await tenantService.DeleteWebhookAsync(tenantId, webhookId);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpGet("{tenantId:guid}/webhooks/{webhookId:guid}/deliveries")]
|
|
public async Task<IActionResult> GetWebhookDeliveries(Guid tenantId, Guid webhookId)
|
|
=> Ok(new { data = await tenantService.GetWebhookDeliveriesAsync(tenantId, webhookId) });
|
|
|
|
private Guid GetUserId() => Guid.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
|
?? User.FindFirst("sub")?.Value ?? throw new UnauthorizedAccessException());
|
|
}
|
|
|
|
[ApiController]
|
|
[Route("v1/api-keys")]
|
|
public class ApiKeyValidationController(ITenantService tenantService) : ControllerBase
|
|
{
|
|
[HttpPost("validate")]
|
|
public async Task<IActionResult> Validate([FromBody] ValidateApiKeyRequest request)
|
|
=> Ok(await tenantService.ValidateApiKeyAsync(request.KeyPrefix, request.KeyHash, request.IpAddress));
|
|
}
|