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 Sockeye.Models; using Sockeye.Api.ControllerHelpers; using Sockeye.Biz; using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Sockeye.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 /// /// Translation controller /// [ApiController] [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/translation")] [Produces("application/json")] [Authorize] public class TranslationController : ControllerBase { private readonly AyContext ct; private readonly ILogger log; private readonly ApiServerState serverState; /// /// ctor /// /// /// /// public TranslationController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } /// /// Get Translation all values /// /// /// A single Translation and it's values [HttpGet("{id}")] public async Task 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)); } /// /// Update Translation /// /// /// /// [HttpPut] public async Task 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 })); ; } /// /// Get Translations list /// /// List in alphabetical order of all Translations [HttpGet("list")] public async Task 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) /// /// Get a coverage report of translation keys used versus unused /// /// Report of all unique translation keys requested since last server reboot [HttpGet("translationkeycoverage")] public async Task 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 /// /// Get subset of translation values /// /// List of translation key strings /// A key value array of translation text values [HttpPost("subset")] public async Task SubSet([FromBody] List 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)); } /// /// Get subset of translation values for specific translation Id /// /// /// List of translation key strings /// A key value array of translation text values [HttpPost("subset/{id}")] [AllowAnonymous] public async Task SubSet([FromRoute] long id, [FromBody] List 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)); } /// /// Duplicate /// /// Source object id /// From route path /// Duplicate [HttpPost("duplicate/{id}")] public async Task DuplicateTranslation([FromRoute] long id, 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)); } /// /// Delete Translation /// /// /// Ok [HttpDelete("{id}")] public async Task 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, SockType.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(); } /// /// Get Translation all values /// /// /// download token /// A single Translation and it's values [AllowAnonymous] [HttpGet("download/{id}")] public async Task 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(Sockeye.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 Sockeye.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; } /// /// Upload Translation export file /// Max 15mb total /// /// Accepted [Authorize] [HttpPost("upload")] [DisableFormValueModelBinding] [RequestSizeLimit(Sockeye.Util.ServerBootConfig.MAX_TRANSLATION_UPLOAD_BYTES)] public async Task 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)); // SockTypeId 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 FileData = new List(); //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), Sockeye.Util.FileUtil.GetBytesReadable(Sockeye.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("SockType")) UploadAType = uploadFormData.FormFieldData["SockType"].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>(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(); } // /// // /// Put (UpdateTranslationItemDisplayText) // /// Update a single key with new display text // /// // /// NewText/Id/Concurrency token object. NewText is new display text, Id is TranslationItem Id, concurrency token is required // /// // [HttpPut("updatetranslationitemdisplaytext")] // public async Task 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, SockType.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 })); // } /// /// Put (UpdateTranslationItemDisplayText) /// Update a list of items with new display text /// /// Array of NewText/Id/Concurrency token objects. NewText is new display text, Id is TranslationItem Id, concurrency token is required /// [HttpPut("updatetranslationitemsdisplaytext")] public async Task PutTranslationItemsDisplayText([FromBody] List 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, SockType.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 RequestedKeys { get; set; } public int RequestedKeyCount { get; set; } public List NotRequestedKeys { get; set; } public int NotRequestedKeyCount { get; set; } public TranslationCoverageInfo() { RequestedKeys = new List(); NotRequestedKeys = new List(); } } #endif } }