예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase) 위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는 인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음. 인증/세션 - 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지 - ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로 로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example) - Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel 기능 복원 (실데이터) - Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력) - Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계 - License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용), 개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록 - Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움 - 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반) - 사용자 상세 티어별 기능 배지(pro_plus 조건부) .NET - SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
486 lines
20 KiB
C#
486 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using D3ROVoice.Api.Data;
|
|
using D3ROVoice.Api.Dtos;
|
|
using D3ROVoice.Api.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace D3ROVoice.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize(Policy = "ManagerOrAbove")]
|
|
public class AdminController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ISttProxyService _sttService;
|
|
private readonly IAdminOperationService _adminOperations;
|
|
private static readonly DateTime _serverStartTime = DateTime.UtcNow;
|
|
|
|
public AdminController(AppDbContext db, ISttProxyService sttService, IAdminOperationService adminOperations)
|
|
{
|
|
_db = db;
|
|
_sttService = sttService;
|
|
_adminOperations = adminOperations;
|
|
}
|
|
|
|
[HttpGet("stats")]
|
|
public async Task<IActionResult> GetStats()
|
|
{
|
|
var totalUsers = await _db.Users.CountAsync();
|
|
var today = DateTime.UtcNow.Date;
|
|
var activeUsersToday = await _db.Users.CountAsync(u => u.LastLoginAt >= today);
|
|
var totalRequests = await _db.UsageLogs.CountAsync() + await _db.SttUsageLogs.CountAsync();
|
|
var totalLlmCost = await _db.UsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
|
var totalSttCost = await _db.SttUsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
|
var totalCost = totalLlmCost + totalSttCost;
|
|
var errorCount = await _db.ErrorLogs.CountAsync();
|
|
|
|
var recentErrors = await _db.ErrorLogs
|
|
.OrderByDescending(e => e.CreatedAt)
|
|
.Take(10)
|
|
.Select(e => new ServerErrorLogDto(e.Id, e.ErrorType, e.Message, e.Endpoint, e.CreatedAt))
|
|
.ToListAsync();
|
|
|
|
var uptime = (DateTime.UtcNow - _serverStartTime).TotalSeconds;
|
|
|
|
var stats = new ServerStatsDto(
|
|
totalUsers,
|
|
activeUsersToday,
|
|
totalRequests,
|
|
totalCost,
|
|
uptime,
|
|
errorCount,
|
|
recentErrors
|
|
);
|
|
|
|
return Ok(stats);
|
|
}
|
|
|
|
[HttpGet("users")]
|
|
public async Task<IActionResult> GetUsers()
|
|
{
|
|
var users = await _db.Users
|
|
.OrderByDescending(u => u.CreatedAt)
|
|
.Select(u => new UserInfoDto(u.Id, u.Email, u.Role, u.CreatedAt, u.LastLoginAt, u.IsActive))
|
|
.ToListAsync();
|
|
|
|
return Ok(users);
|
|
}
|
|
|
|
// ── LLM Model Endpoints ───────────────────────────────────────────────
|
|
|
|
[HttpGet("endpoints")]
|
|
public async Task<IActionResult> GetEndpoints()
|
|
{
|
|
var endpoints = await _db.ModelEndpoints
|
|
.AsNoTracking()
|
|
.OrderBy(m => m.Id)
|
|
.Select(endpoint => new
|
|
{
|
|
endpoint.Id,
|
|
endpoint.ModelId,
|
|
endpoint.ModelName,
|
|
endpoint.Provider,
|
|
endpoint.EndpointUrl,
|
|
ApiKey = endpoint.ApiKey == "" ? "" : "••••••••",
|
|
endpoint.CostPer1kPromptTokens,
|
|
endpoint.CostPer1kCompletionTokens,
|
|
endpoint.IsActive,
|
|
endpoint.CreatedAt
|
|
})
|
|
.ToListAsync();
|
|
|
|
return Ok(endpoints);
|
|
}
|
|
|
|
[HttpPost("endpoints")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> CreateEndpoint([FromBody] CreateModelEndpointDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.ModelId) || string.IsNullOrWhiteSpace(dto.ModelName)
|
|
|| string.IsNullOrWhiteSpace(dto.Provider) || !IsValidEndpointUrl(dto.EndpointUrl)
|
|
|| dto.CostPer1kPromptTokens < 0 || dto.CostPer1kCompletionTokens < 0)
|
|
{
|
|
return BadRequest(new { message = "모델 식별자, 공급자, 유효한 HTTP(S) URL과 0 이상의 비용이 필요합니다." });
|
|
}
|
|
|
|
return await ExecuteAdminMutationAsync(
|
|
"model_endpoint.create",
|
|
dto,
|
|
"model_endpoint",
|
|
_ => dto.ModelId.Trim(),
|
|
dto.Memo,
|
|
() => Task.FromResult<object?>(null),
|
|
async () =>
|
|
{
|
|
if (await _db.ModelEndpoints.AnyAsync(model => model.ModelId == dto.ModelId.Trim()))
|
|
throw new AdminOperationException("model_id_already_exists");
|
|
var endpoint = new ServiceModelEndpoint
|
|
{
|
|
ModelId = dto.ModelId.Trim(),
|
|
ModelName = dto.ModelName.Trim(),
|
|
Provider = dto.Provider.Trim(),
|
|
EndpointUrl = dto.EndpointUrl.Trim(),
|
|
ApiKey = dto.ApiKey?.Trim() ?? "",
|
|
CostPer1kPromptTokens = dto.CostPer1kPromptTokens,
|
|
CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
_db.ModelEndpoints.Add(endpoint);
|
|
await _db.SaveChangesAsync();
|
|
return ToModelEndpointDto(endpoint);
|
|
});
|
|
}
|
|
|
|
[HttpPut("endpoints/{id}")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> UpdateEndpoint(int id, [FromBody] UpdateModelEndpointDto dto)
|
|
{
|
|
if (id <= 0 || string.IsNullOrWhiteSpace(dto.ModelName) || string.IsNullOrWhiteSpace(dto.Provider)
|
|
|| !IsValidEndpointUrl(dto.EndpointUrl) || dto.CostPer1kPromptTokens < 0 || dto.CostPer1kCompletionTokens < 0)
|
|
return BadRequest(new { message = "유효한 모델 엔드포인트 값이 필요합니다." });
|
|
|
|
return await ExecuteAdminMutationAsync(
|
|
"model_endpoint.update",
|
|
new { id, dto },
|
|
"model_endpoint",
|
|
_ => id.ToString(),
|
|
dto.Memo,
|
|
async () => await _db.ModelEndpoints.AsNoTracking().Where(endpoint => endpoint.Id == id)
|
|
.Select(endpoint => new { endpoint.Id, endpoint.ModelId, endpoint.ModelName, endpoint.Provider, endpoint.EndpointUrl, endpoint.IsActive })
|
|
.SingleOrDefaultAsync(),
|
|
async () =>
|
|
{
|
|
var endpoint = await _db.ModelEndpoints.FindAsync(id)
|
|
?? throw new AdminOperationException("model_endpoint_not_found");
|
|
endpoint.ModelName = dto.ModelName.Trim();
|
|
endpoint.Provider = dto.Provider.Trim();
|
|
endpoint.EndpointUrl = dto.EndpointUrl.Trim();
|
|
if (dto.ApiKey != null) endpoint.ApiKey = dto.ApiKey.Trim();
|
|
endpoint.CostPer1kPromptTokens = dto.CostPer1kPromptTokens;
|
|
endpoint.CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens;
|
|
endpoint.IsActive = dto.IsActive;
|
|
await _db.SaveChangesAsync();
|
|
return ToModelEndpointDto(endpoint);
|
|
});
|
|
}
|
|
|
|
[HttpDelete("endpoints/{id}")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> DeleteEndpoint(int id, [FromBody] AdminActionDto dto)
|
|
{
|
|
if (id <= 0) return BadRequest();
|
|
return await ExecuteAdminMutationAsync(
|
|
"model_endpoint.delete",
|
|
new { id, dto.Memo },
|
|
"model_endpoint",
|
|
_ => id.ToString(),
|
|
dto.Memo,
|
|
async () => await _db.ModelEndpoints.AsNoTracking().Where(endpoint => endpoint.Id == id)
|
|
.Select(endpoint => new { endpoint.Id, endpoint.ModelId, endpoint.ModelName, endpoint.Provider, endpoint.EndpointUrl, endpoint.IsActive })
|
|
.SingleOrDefaultAsync(),
|
|
async () =>
|
|
{
|
|
var endpoint = await _db.ModelEndpoints.FindAsync(id)
|
|
?? throw new AdminOperationException("model_endpoint_not_found");
|
|
_db.ModelEndpoints.Remove(endpoint);
|
|
await _db.SaveChangesAsync();
|
|
return new { message = "삭제되었습니다.", id };
|
|
});
|
|
}
|
|
|
|
// ── STT / Transcription Provider Endpoints ────────────────────────────
|
|
|
|
[HttpGet("stt-endpoints")]
|
|
public async Task<IActionResult> GetSttEndpoints()
|
|
{
|
|
var endpoints = await _sttService.GetAllEndpointsAsync();
|
|
return Ok(endpoints);
|
|
}
|
|
|
|
[HttpGet("stt-endpoints/{id}")]
|
|
public async Task<IActionResult> GetSttEndpoint(int id)
|
|
{
|
|
var endpoints = await _sttService.GetAllEndpointsAsync();
|
|
var endpoint = endpoints.FirstOrDefault(e => e.Id == id);
|
|
if (endpoint == null) return NotFound(new { message = $"STT Endpoint {id} not found." });
|
|
return Ok(endpoint);
|
|
}
|
|
|
|
[HttpPost("stt-endpoints")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> CreateSttEndpoint([FromBody] CreateSttEndpointDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.Name) || !IsValidEndpointUrl(dto.EndpointUrl)
|
|
|| dto.CostPerMinute < 0 || dto.CostPerSecond < 0 || dto.FallbackPriority < 1)
|
|
{
|
|
return BadRequest(new { message = "유효한 STT 이름, HTTP(S) URL, 비용, 우선순위가 필요합니다." });
|
|
}
|
|
|
|
return await ExecuteAdminMutationAsync(
|
|
"stt_endpoint.create",
|
|
dto,
|
|
"stt_endpoint",
|
|
result => ((SttProviderEndpointDto)result).Id.ToString(),
|
|
dto.Memo,
|
|
() => Task.FromResult<object?>(null),
|
|
async () => await _sttService.CreateEndpointAsync(dto));
|
|
}
|
|
|
|
[HttpPut("stt-endpoints/{id}")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> UpdateSttEndpoint(int id, [FromBody] UpdateSttEndpointDto dto)
|
|
{
|
|
if (id <= 0 || string.IsNullOrWhiteSpace(dto.Name) || !IsValidEndpointUrl(dto.EndpointUrl)
|
|
|| dto.CostPerMinute < 0 || dto.CostPerSecond < 0 || dto.FallbackPriority < 1)
|
|
return BadRequest(new { message = "유효한 STT 엔드포인트 값이 필요합니다." });
|
|
|
|
return await ExecuteAdminMutationAsync(
|
|
"stt_endpoint.update",
|
|
new { id, dto },
|
|
"stt_endpoint",
|
|
_ => id.ToString(),
|
|
dto.Memo,
|
|
async () => await _db.SttProviderEndpoints.AsNoTracking().Where(endpoint => endpoint.Id == id)
|
|
.Select(endpoint => new { endpoint.Id, endpoint.Name, endpoint.ProviderType, endpoint.EndpointUrl, endpoint.ModelId, endpoint.IsDefault, endpoint.IsActive, endpoint.FallbackPriority })
|
|
.SingleOrDefaultAsync(),
|
|
async () =>
|
|
{
|
|
try { return await _sttService.UpdateEndpointAsync(id, dto); }
|
|
catch (KeyNotFoundException) { throw new AdminOperationException("stt_endpoint_not_found"); }
|
|
});
|
|
}
|
|
|
|
[HttpDelete("stt-endpoints/{id}")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> DeleteSttEndpoint(int id, [FromBody] AdminActionDto dto)
|
|
{
|
|
if (id <= 0) return BadRequest();
|
|
return await ExecuteAdminMutationAsync(
|
|
"stt_endpoint.delete",
|
|
new { id, dto.Memo },
|
|
"stt_endpoint",
|
|
_ => id.ToString(),
|
|
dto.Memo,
|
|
async () => await _db.SttProviderEndpoints.AsNoTracking().Where(endpoint => endpoint.Id == id)
|
|
.Select(endpoint => new { endpoint.Id, endpoint.Name, endpoint.ProviderType, endpoint.EndpointUrl, endpoint.ModelId, endpoint.IsDefault, endpoint.IsActive, endpoint.FallbackPriority })
|
|
.SingleOrDefaultAsync(),
|
|
async () =>
|
|
{
|
|
if (!await _sttService.DeleteEndpointAsync(id)) throw new AdminOperationException("stt_endpoint_not_found");
|
|
return new { message = "STT 엔드포인트가 성공적으로 삭제되었습니다.", id };
|
|
});
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/{id}/set-default")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> SetDefaultSttEndpoint(int id, [FromBody] AdminActionDto dto)
|
|
{
|
|
if (id <= 0) return BadRequest();
|
|
return await ExecuteAdminMutationAsync(
|
|
"stt_endpoint.set_default",
|
|
new { id, dto.Memo },
|
|
"stt_endpoint",
|
|
_ => id.ToString(),
|
|
dto.Memo,
|
|
async () => await _db.SttProviderEndpoints.AsNoTracking().Where(endpoint => endpoint.IsDefault)
|
|
.Select(endpoint => new { endpoint.Id, endpoint.Name }).ToListAsync(),
|
|
async () =>
|
|
{
|
|
if (!await _sttService.SetDefaultEndpointAsync(id)) throw new AdminOperationException("stt_endpoint_not_found");
|
|
return new { id, isDefault = true, success = true, message = "기본 클라우드 전사 프로바이더로 설정되었습니다." };
|
|
});
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/{id}/test")]
|
|
public async Task<IActionResult> TestSttEndpoint(int id)
|
|
{
|
|
var result = await _sttService.TestEndpointAsync(id);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/test-direct")]
|
|
[Authorize(Policy = "AdminOrAbove")]
|
|
public async Task<IActionResult> TestSttDirect([FromBody] DirectSttTestDto dto)
|
|
{
|
|
if (!IsValidEndpointUrl(dto.EndpointUrl) || !Uri.TryCreate(dto.EndpointUrl, UriKind.Absolute, out var endpointUri))
|
|
{
|
|
return BadRequest(new { message = "유효한 HTTP(S) Endpoint URL이 필요합니다." });
|
|
}
|
|
|
|
var result = await _sttService.TestEndpointAsync(0, dto.ApiKey, endpointUri.AbsoluteUri);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("stt-usage")]
|
|
public async Task<IActionResult> GetSttUsageReport()
|
|
{
|
|
var report = await _sttService.GetUsageReportAsync();
|
|
return Ok(report);
|
|
}
|
|
|
|
// ── LLM Usage Report ──────────────────────────────────────────────────
|
|
|
|
[HttpGet("usage")]
|
|
public async Task<IActionResult> GetUsageReport()
|
|
{
|
|
var logs = await _db.UsageLogs.AsNoTracking().ToListAsync();
|
|
|
|
var totalRequests = logs.Count;
|
|
var totalPromptTokens = logs.Sum(l => l.PromptTokens);
|
|
var totalCompletionTokens = logs.Sum(l => l.CompletionTokens);
|
|
var totalCost = logs.Sum(l => l.CalculatedCost);
|
|
|
|
var userSummaries = logs
|
|
.GroupBy(l => new { l.UserId, l.UserEmail })
|
|
.Select(g => new UserUsageSummaryDto(
|
|
g.Key.UserId,
|
|
g.Key.UserEmail,
|
|
g.Count(),
|
|
g.Sum(x => x.TotalTokens),
|
|
g.Sum(x => x.CalculatedCost)
|
|
))
|
|
.OrderByDescending(u => u.TotalCost)
|
|
.ToList();
|
|
|
|
var modelSummaries = logs
|
|
.GroupBy(l => l.ModelId)
|
|
.Select(g => new ModelUsageSummaryDto(
|
|
g.Key,
|
|
g.Key,
|
|
g.Count(),
|
|
g.Sum(x => x.TotalTokens),
|
|
g.Sum(x => x.CalculatedCost)
|
|
))
|
|
.OrderByDescending(m => m.TotalCost)
|
|
.ToList();
|
|
|
|
var report = new UsageReportDto(
|
|
totalRequests,
|
|
totalPromptTokens,
|
|
totalCompletionTokens,
|
|
totalCost,
|
|
userSummaries,
|
|
modelSummaries
|
|
);
|
|
|
|
return Ok(report);
|
|
}
|
|
|
|
// 오프라인 Ed25519 라이선스 발급 감사. admin 콘솔이 서명 후 이 엔드포인트로 기록만 남긴다.
|
|
[HttpPost("license-audit")]
|
|
[Authorize(Policy = "SuperAdminOnly")]
|
|
[RequestSizeLimit(8 * 1024)]
|
|
public async Task<IActionResult> RecordLicenseAudit([FromBody] LicenseAuditDto dto)
|
|
{
|
|
var actorEmail = User.FindFirstValue(ClaimTypes.Email)?.Trim().ToLowerInvariant() ?? string.Empty;
|
|
if (actorEmail.Length is < 3 or > 150) return Unauthorized(new { error = "invalid_actor" });
|
|
|
|
var licenseId = dto.LicenseId?.Trim() ?? string.Empty;
|
|
var customerEmail = dto.CustomerEmail?.Trim().ToLowerInvariant() ?? string.Empty;
|
|
var tier = dto.Tier?.Trim() ?? string.Empty;
|
|
var validity = dto.Validity?.Trim() ?? string.Empty;
|
|
if (licenseId.Length is < 3 or > 200) return BadRequest(new { error = "invalid_license_id" });
|
|
if (customerEmail.Length is < 3 or > 150 || !customerEmail.Contains('@')) return BadRequest(new { error = "invalid_customer_email" });
|
|
if (tier is not ("pro" or "pro_plus" or "team" or "enterprise")) return BadRequest(new { error = "invalid_tier" });
|
|
if (validity is not ("30d" or "365d" or "lifetime")) return BadRequest(new { error = "invalid_validity" });
|
|
|
|
var afterJson = JsonSerializer.Serialize(new
|
|
{
|
|
licenseId,
|
|
customerEmail,
|
|
tier,
|
|
validity,
|
|
expiresAt = dto.ExpiresAt
|
|
});
|
|
|
|
_db.AdminAuditEntries.Add(new AdminAuditEntry
|
|
{
|
|
ActorEmail = actorEmail,
|
|
Action = "license.issue",
|
|
TargetType = "license",
|
|
TargetId = licenseId,
|
|
AfterJson = afterJson,
|
|
Memo = $"Issued {tier} license ({validity}) for {customerEmail}",
|
|
IdempotencyKey = Guid.NewGuid().ToString("D"),
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new { success = true });
|
|
}
|
|
|
|
private static object ToModelEndpointDto(ServiceModelEndpoint endpoint) => new
|
|
{
|
|
endpoint.Id,
|
|
endpoint.ModelId,
|
|
endpoint.ModelName,
|
|
endpoint.Provider,
|
|
endpoint.EndpointUrl,
|
|
ApiKey = string.IsNullOrWhiteSpace(endpoint.ApiKey) ? string.Empty : "••••••••",
|
|
endpoint.CostPer1kPromptTokens,
|
|
endpoint.CostPer1kCompletionTokens,
|
|
endpoint.IsActive,
|
|
endpoint.CreatedAt
|
|
};
|
|
|
|
private static bool IsValidEndpointUrl(string? value)
|
|
{
|
|
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || !string.IsNullOrEmpty(uri.UserInfo)) return false;
|
|
if (uri.Scheme == Uri.UriSchemeHttp) return uri.IsLoopback;
|
|
return uri.Scheme == Uri.UriSchemeHttps;
|
|
}
|
|
|
|
private async Task<IActionResult> ExecuteAdminMutationAsync(
|
|
string operation,
|
|
object request,
|
|
string targetType,
|
|
Func<object, string> targetId,
|
|
string? memo,
|
|
Func<Task<object?>> readBefore,
|
|
Func<Task<object>> mutate)
|
|
{
|
|
var actorEmail = User.FindFirstValue(ClaimTypes.Email);
|
|
if (string.IsNullOrWhiteSpace(actorEmail)) return Unauthorized(new { message = "관리자 이메일 claim이 필요합니다." });
|
|
|
|
var idempotencyKey = Request.Headers["Idempotency-Key"].ToString();
|
|
try
|
|
{
|
|
var result = await _adminOperations.ExecuteAsync(
|
|
actorEmail,
|
|
operation,
|
|
idempotencyKey,
|
|
request,
|
|
targetType,
|
|
targetId,
|
|
memo ?? string.Empty,
|
|
readBefore,
|
|
mutate);
|
|
return Ok(result);
|
|
}
|
|
catch (AdminOperationException ex) when (ex.Message == "idempotency_key_reused_with_different_request")
|
|
{
|
|
return Conflict(new { message = ex.Message });
|
|
}
|
|
catch (AdminOperationException ex) when (ex.Message.EndsWith("_not_found", StringComparison.Ordinal))
|
|
{
|
|
return NotFound(new { message = ex.Message });
|
|
}
|
|
catch (AdminOperationException ex)
|
|
{
|
|
return BadRequest(new { message = ex.Message });
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
return Conflict(new { message = "admin_operation_conflict" });
|
|
}
|
|
}
|
|
}
|