Files
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
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>
2026-05-29 23:29:31 +03:30

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));
}