report/upload and translation/upload had no role check, so any login, customer contacts included, could import a report template (whose JavaScript runs in the server's browser at render time) or a translation (security review F19, F24). Both now require the same right as creating one, checked before the upload is read. The import screens are already limited to those roles. Checked locally: customer contact and Service 403, BizAdmin 202. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
572 lines
23 KiB
C#
572 lines
23 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
using AyaNova.Models;
|
|
using AyaNova.Api.ControllerHelpers;
|
|
using AyaNova.Biz;
|
|
using System;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
|
|
namespace AyaNova.Api.Controllers
|
|
{
|
|
//DOCUMENTATING THE API
|
|
//https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/recommended-tags-for-documentation-comments
|
|
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments
|
|
|
|
/// <summary>
|
|
/// Translation controller
|
|
/// </summary>
|
|
[ApiController]
|
|
[Asp.Versioning.ApiVersion("8.0")]
|
|
[Route("api/v{version:apiVersion}/translation")]
|
|
[Produces("application/json")]
|
|
[Authorize]
|
|
public class TranslationController : ControllerBase
|
|
{
|
|
private readonly AyContext ct;
|
|
private readonly ILogger<TranslationController> log;
|
|
private readonly ApiServerState serverState;
|
|
|
|
|
|
/// <summary>
|
|
/// ctor
|
|
/// </summary>
|
|
/// <param name="dbcontext"></param>
|
|
/// <param name="logger"></param>
|
|
/// <param name="apiServerState"></param>
|
|
public TranslationController(AyContext dbcontext, ILogger<TranslationController> logger, ApiServerState apiServerState)
|
|
{
|
|
ct = dbcontext;
|
|
log = logger;
|
|
serverState = apiServerState;
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get Translation all values
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <returns>A single Translation and it's values</returns>
|
|
[HttpGet("{id}")]
|
|
public async Task<IActionResult> GetTranslation([FromRoute] long id)
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
}
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
var o = await biz.GetAsync(id);
|
|
|
|
if (o == null)
|
|
{
|
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
}
|
|
|
|
return Ok(ApiOkResponse.Response(o));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Update Translation
|
|
///
|
|
/// </summary>
|
|
/// <param name="updatedObject"></param>
|
|
/// <returns></returns>
|
|
[HttpPut]
|
|
public async Task<IActionResult> PutTranslation([FromBody] Translation updatedObject)
|
|
{
|
|
if (!serverState.IsOpen)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
|
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
var o = await biz.PutAsync(updatedObject);
|
|
if (o == null)
|
|
{
|
|
if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT))
|
|
return StatusCode(409, new ApiErrorResponse(biz.Errors));
|
|
else
|
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
}
|
|
return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); ;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get Translations list
|
|
/// </summary>
|
|
/// <returns>List in alphabetical order of all Translations</returns>
|
|
[HttpGet("list")]
|
|
public async Task<IActionResult> TranslationList()
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
var l = await biz.GetTranslationListAsync();
|
|
return Ok(ApiOkResponse.Response(l));
|
|
}
|
|
|
|
|
|
#if (DEBUG)
|
|
/// <summary>
|
|
/// Get a coverage report of translation keys used versus unused
|
|
/// </summary>
|
|
/// <returns>Report of all unique translation keys requested since last server reboot</returns>
|
|
[HttpGet("translationkeycoverage")]
|
|
public async Task<IActionResult> TranslationKeyCoverage()
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
var l = await biz.TranslationKeyCoverageAsync();
|
|
return Ok(ApiOkResponse.Response(l));
|
|
}
|
|
#endif
|
|
|
|
|
|
/// <summary>
|
|
/// Get subset of translation values
|
|
/// </summary>
|
|
/// <param name="inObj">List of translation key strings</param>
|
|
/// <returns>A key value array of translation text values</returns>
|
|
[HttpPost("subset")]
|
|
public async Task<IActionResult> SubSet([FromBody] List<string> inObj)
|
|
{
|
|
if (serverState.IsClosed)
|
|
{
|
|
//Exception for SuperUser account to handle licensing issues
|
|
if (UserIdFromContext.Id(HttpContext.Items) != 1)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
}
|
|
|
|
//Instantiate the business object handler
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
var l = await biz.GetSubsetAsync(inObj);
|
|
return Ok(ApiOkResponse.Response(l));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get subset of translation values for specific translation Id
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <param name="inObj">List of translation key strings</param>
|
|
/// <returns>A key value array of translation text values</returns>
|
|
[HttpPost("subset/{id}")]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> SubSet([FromRoute] long id, [FromBody] List<string> inObj)
|
|
{
|
|
//## NOTE: This route is ONLY used at present for the reset password form at the client
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
var l = await TranslationBiz.GetSpecifiedTranslationSubsetStaticAsync(inObj, id);
|
|
return Ok(ApiOkResponse.Response(l));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Duplicate
|
|
/// </summary>
|
|
/// <param name="id">Source object id</param>
|
|
/// <param name="apiVersion">From route path</param>
|
|
/// <returns>Duplicate</returns>
|
|
[HttpPost("duplicate/{id}")]
|
|
public async Task<IActionResult> DuplicateTranslation([FromRoute] long id, Asp.Versioning.ApiVersion apiVersion)
|
|
{
|
|
if (!serverState.IsOpen)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType))
|
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
Translation o = await biz.DuplicateAsync(id);
|
|
if (o == null)
|
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
else
|
|
return CreatedAtAction(nameof(TranslationController.GetTranslation), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o));
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Delete Translation
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <returns>Ok</returns>
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> DeleteTranslation([FromRoute] long id)
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
}
|
|
|
|
|
|
//Fetch translation and it's children
|
|
//(fetch here so can return proper REST responses on failing basic validity)
|
|
var dbObject = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == id);
|
|
if (dbObject == null)
|
|
{
|
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
}
|
|
|
|
if (!Authorized.HasDeleteRole(HttpContext.Items, AyaType.Translation))
|
|
{
|
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
}
|
|
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
if (!await biz.DeleteAsync(dbObject))
|
|
{
|
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
}
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get Translation all values
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <param name="t">download token</param>
|
|
/// <returns>A single Translation and it's values</returns>
|
|
[AllowAnonymous]
|
|
[HttpGet("download/{id}")]
|
|
public async Task<IActionResult> DownloadTranslation([FromRoute] long id, [FromQuery] string t)
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
|
|
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
|
|
{
|
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
|
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
|
}
|
|
|
|
var o = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == id);
|
|
//turn into correct format and then send as file
|
|
if (o == null)
|
|
{
|
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
}
|
|
var asText = Newtonsoft.Json.JsonConvert.SerializeObject(
|
|
o,
|
|
Newtonsoft.Json.Formatting.None,
|
|
new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "TranslationId" }) });
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes(asText);
|
|
var file = new FileContentResult(bytes, "application/octet-stream");
|
|
file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".json";
|
|
return file;
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Upload Translation export file
|
|
/// Max 15mb total
|
|
/// </summary>
|
|
/// <returns>Accepted</returns>
|
|
[Authorize]
|
|
[HttpPost("upload")]
|
|
[DisableFormValueModelBinding]
|
|
[RequestSizeLimit(AyaNova.Util.ServerBootConfig.MAX_TRANSLATION_UPLOAD_BYTES)]
|
|
public async Task<IActionResult> UploadAsync()
|
|
{
|
|
//Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
|
|
|
|
if (!serverState.IsOpen)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
//case 4718: importing a translation creates one, so it needs the same right as creating one (checked before the upload is read)
|
|
if (!Authorized.HasCreateRole(HttpContext.Items, AyaType.Translation))
|
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
|
|
// AyaTypeId attachToObject = null;
|
|
ApiUploadProcessor.ApiUploadedFilesResult uploadFormData = null;
|
|
try
|
|
{
|
|
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
|
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}"));
|
|
|
|
|
|
bool badRequest = false;
|
|
string UploadAType = string.Empty;
|
|
string UploadObjectId = string.Empty;
|
|
string errorMessage = string.Empty;
|
|
string Notes = string.Empty;
|
|
List<UploadFileData> FileData = new List<UploadFileData>();
|
|
|
|
//Save uploads to disk under temporary file names until we decide how to handle them
|
|
uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
|
|
if (!string.IsNullOrWhiteSpace(uploadFormData.Error))
|
|
{
|
|
errorMessage = uploadFormData.Error;
|
|
//delete temp files
|
|
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
|
|
//file too large is most likely issue so in that case return this localized properly
|
|
if (errorMessage.Contains("413"))
|
|
{
|
|
var TransId = UserTranslationIdFromContext.Id(HttpContext.Items);
|
|
return BadRequest(new ApiErrorResponse(
|
|
ApiErrorCode.VALIDATION_LENGTH_EXCEEDED,
|
|
null,
|
|
String.Format(await TranslationBiz.GetTranslationStaticAsync("AyaFileFileTooLarge", TransId, ct), AyaNova.Util.FileUtil.GetBytesReadable(AyaNova.Util.ServerBootConfig.MAX_TRANSLATION_UPLOAD_BYTES))));
|
|
}
|
|
else//not too big, something else
|
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage));
|
|
}
|
|
|
|
|
|
if (
|
|
!uploadFormData.FormFieldData.ContainsKey("FileData"))//only filedata is required
|
|
{
|
|
badRequest = true;
|
|
errorMessage = "Missing required FormFieldData value: FileData";
|
|
}
|
|
if (!badRequest)
|
|
{
|
|
if (uploadFormData.FormFieldData.ContainsKey("AType"))
|
|
UploadAType = uploadFormData.FormFieldData["AType"].ToString();
|
|
if (uploadFormData.FormFieldData.ContainsKey("ObjectId"))
|
|
UploadObjectId = uploadFormData.FormFieldData["ObjectId"].ToString();
|
|
if (uploadFormData.FormFieldData.ContainsKey("Notes"))
|
|
Notes = uploadFormData.FormFieldData["Notes"].ToString();
|
|
//fileData in JSON stringify format which contains the actual last modified dates etc
|
|
//"[{\"name\":\"Client.csv\",\"lastModified\":1582822079618},{\"name\":\"wmi4fu06nrs41.jpg\",\"lastModified\":1586900220990}]"
|
|
FileData = Newtonsoft.Json.JsonConvert.DeserializeObject<List<UploadFileData>>(uploadFormData.FormFieldData["FileData"].ToString());
|
|
|
|
}
|
|
|
|
|
|
// long UserId = UserIdFromContext.Id(HttpContext.Items);
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
|
|
//We have our files now can parse and insert into db
|
|
if (uploadFormData.UploadedFiles.Count > 0)
|
|
{
|
|
//deserialize each file and import
|
|
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
|
|
{
|
|
JObject o = JObject.Parse(System.IO.File.ReadAllText(a.InitialUploadedPathName));
|
|
if (!await biz.ImportAsync(o))
|
|
{
|
|
//delete all the files temporarily uploaded and return bad request
|
|
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
|
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (System.IO.InvalidDataException ex)
|
|
{
|
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
|
|
}
|
|
finally
|
|
{
|
|
//delete all the files temporarily uploaded and return bad request
|
|
|
|
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
|
|
}
|
|
|
|
//Return nothing
|
|
return Accepted();
|
|
}
|
|
|
|
|
|
// /// <summary>
|
|
// /// Put (UpdateTranslationItemDisplayText)
|
|
// /// Update a single key with new display text
|
|
// /// </summary>
|
|
// /// <param name="inObj">NewText/Id/Concurrency token object. NewText is new display text, Id is TranslationItem Id, concurrency token is required</param>
|
|
// /// <returns></returns>
|
|
// [HttpPut("updatetranslationitemdisplaytext")]
|
|
// public async Task<IActionResult> PutTranslationItemDisplayText([FromBody] NewTextIdConcurrencyTokenItem inObj)
|
|
// {
|
|
// if (serverState.IsClosed)
|
|
// return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
// if (!ModelState.IsValid)
|
|
// {
|
|
// return BadRequest(new ApiErrorResponse(ModelState));
|
|
// }
|
|
|
|
// var oFromDb = await ct.TranslationItem.SingleOrDefaultAsync(z => z.Id == inObj.Id);
|
|
|
|
// if (oFromDb == null)
|
|
// {
|
|
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
// }
|
|
|
|
// //Now fetch translation for rights and to ensure not stock
|
|
// var oDbParent = await ct.Translation.SingleOrDefaultAsync(z => z.Id == oFromDb.TranslationId);
|
|
// if (oDbParent == null)
|
|
// {
|
|
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
// }
|
|
|
|
// if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Translation))
|
|
// {
|
|
// return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
// }
|
|
|
|
// //Instantiate the business object handler
|
|
// TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
// try
|
|
// {
|
|
// if (!await biz.PutTranslationItemDisplayTextAsync(oFromDb, inObj, oDbParent))
|
|
// {
|
|
// return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
// }
|
|
// }
|
|
// catch (DbUpdateConcurrencyException)
|
|
// {
|
|
// if (!await biz.TranslationItemExistsAsync(inObj.Id))
|
|
// {
|
|
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
// }
|
|
// else
|
|
// {
|
|
// //exists but was changed by another user
|
|
// //I considered returning new and old record, but where would it end?
|
|
// //Better to let the client decide what to do than to send extra data that is not required
|
|
// return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
|
|
// }
|
|
// }
|
|
|
|
|
|
// return Ok(ApiOkResponse.Response(new { Concurrency = oFromDb.Concurrency }));
|
|
// }
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Put (UpdateTranslationItemDisplayText)
|
|
/// Update a list of items with new display text
|
|
/// </summary>
|
|
/// <param name="inObj">Array of NewText/Id/Concurrency token objects. NewText is new display text, Id is TranslationItem Id, concurrency token is required</param>
|
|
/// <returns></returns>
|
|
[HttpPut("updatetranslationitemsdisplaytext")]
|
|
public async Task<IActionResult> PutTranslationItemsDisplayText([FromBody] List<NewTextIdConcurrencyTokenItem> inObj)
|
|
{
|
|
if (serverState.IsClosed)
|
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(new ApiErrorResponse(ModelState));
|
|
}
|
|
|
|
var oFromDb = await ct.TranslationItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == inObj[0].Id);
|
|
|
|
if (oFromDb == null)
|
|
{
|
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
}
|
|
|
|
//Now fetch translation for rights and to ensure not stock
|
|
var oDbParent = await ct.Translation.SingleOrDefaultAsync(z => z.Id == oFromDb.TranslationId);
|
|
if (oDbParent == null)
|
|
{
|
|
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
|
}
|
|
|
|
if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Translation))
|
|
{
|
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
|
}
|
|
|
|
//Instantiate the business object handler
|
|
TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext);
|
|
|
|
try
|
|
{
|
|
if (!await biz.PutTranslationItemsDisplayTextAsync(inObj, oDbParent))
|
|
{
|
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
|
}
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
|
|
//exists but was changed by another user
|
|
//I considered returning new and old record, but where would it end?
|
|
//Better to let the client decide what to do than to send extra data that is not required
|
|
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
|
|
}
|
|
return NoContent();
|
|
}
|
|
|
|
|
|
|
|
|
|
#if (DEBUG)
|
|
public class TranslationCoverageInfo
|
|
{
|
|
public List<string> RequestedKeys { get; set; }
|
|
public int RequestedKeyCount { get; set; }
|
|
public List<string> NotRequestedKeys { get; set; }
|
|
public int NotRequestedKeyCount { get; set; }
|
|
|
|
public TranslationCoverageInfo()
|
|
{
|
|
RequestedKeys = new List<string>();
|
|
NotRequestedKeys = new List<string>();
|
|
}
|
|
|
|
}
|
|
#endif
|
|
|
|
}
|
|
} |