// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Assets; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; using Squidex.Shared; using Squidex.Web; #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. /// [ApiExplorerSettings(GroupName = nameof(Apps))] public sealed class AppsController : ApiController { private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 50, Height = 50, Mode = ResizeMode.Crop }; private readonly IAppImageStore appImageStore; private readonly IAppProvider appProvider; private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AppsController(ICommandBus commandBus, IAppImageStore appImageStore, IAppProvider appProvider, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { this.appImageStore = appImageStore; this.appProvider = appProvider; this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } /// /// Get your apps. /// /// /// 200 => Apps returned. /// /// /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). /// You will retrieve all apps, where you are assigned as a contributor. /// [HttpGet] [Route("apps/")] [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public async Task GetApps() { var userOrClientId = HttpContext.User.UserOrClientId()!; var userPermissions = Resources.Context.UserPermissions; var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); var response = Deferred.Response(() => { var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, isFrontend, Resources)).ToArray(); }); Response.Headers[HeaderNames.ETag] = apps.ToEtag(); return Ok(response); } /// /// Get an app by name. /// /// The name of the app. /// /// 200 => Apps returned. /// 404 => App not found. /// [HttpGet] [Route("apps/{app}")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public IActionResult GetApp(string app) { var response = Deferred.Response(() => { var userOrClientId = HttpContext.User.UserOrClientId()!; var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); return AppDto.FromApp(App, userOrClientId, isFrontend, Resources); }); Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } /// /// Create a new app. /// /// The app object that needs to be added to Squidex. /// /// 201 => App created. /// 400 => App request not valid. /// 409 => App name is already in use. /// /// /// You can only create an app when you are authenticated as a user (OpenID implicit flow). /// You will be assigned as owner of the new app automatically. /// [HttpPost] [Route("apps/")] [ProducesResponseType(typeof(AppDto), 201)] [ApiPermission] [ApiCosts(0)] public async Task PostApp([FromBody] CreateAppDto request) { var response = await InvokeCommandAsync(request.ToCommand()); return CreatedAtAction(nameof(GetApps), response); } /// /// Update the app. /// /// The name of the app to update. /// The values to update. /// /// 200 => App updated. /// 400 => App request not valid. /// 404 => App not found. /// [HttpPut] [Route("apps/{app}/")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUpdate)] [ApiCosts(0)] public async Task PutApp(string app, [FromBody] UpdateAppDto request) { var response = await InvokeCommandAsync(request.ToCommand()); return Ok(response); } /// /// Upload the app image. /// /// The name of the app to update. /// The file to upload. /// /// 200 => App image uploaded. /// 400 => App request not valid. /// 404 => App not found. /// [HttpPost] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppImageUpload)] [ApiCosts(0)] public async Task UploadImage(string app, IFormFile file) { var response = await InvokeCommandAsync(CreateCommand(file)); return Ok(response); } /// /// Get the app image. /// /// The name of the app. /// /// 200 => App image found and content or (resized) image returned. /// 404 => App not found. /// [HttpGet] [Route("apps/{app}/image")] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [AllowAnonymous] [ApiCosts(0)] public IActionResult GetImage(string app) { if (App.Image == null) { return NotFound(); } var etag = App.Image.Etag; Response.Headers[HeaderNames.ETag] = etag; var callback = new FileCallback(async (body, range, ct) => { var resizedAsset = $"{App.Id}_{etag}_Resized"; try { await assetStore.DownloadAsync(resizedAsset, body, ct: ct); } catch (AssetNotFoundException) { using (Telemetry.Activities.StartActivity("Resize")) { using (var sourceStream = GetTempStream()) { using (var destinationStream = GetTempStream()) { using (Telemetry.Activities.StartActivity("ResizeDownload")) { await appImageStore.DownloadAsync(App.Id, sourceStream); sourceStream.Position = 0; } using (Telemetry.Activities.StartActivity("ResizeImage")) { await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, ResizeOptions); destinationStream.Position = 0; } using (Telemetry.Activities.StartActivity("ResizeUpload")) { await assetStore.UploadAsync(resizedAsset, destinationStream); destinationStream.Position = 0; } await destinationStream.CopyToAsync(body, ct); } } } } }); return new FileCallbackResult(App.Image.MimeType, callback) { ErrorAs404 = true }; } /// /// Remove the app image. /// /// The name of the app to update. /// /// 200 => App image removed. /// 404 => App not found. /// [HttpDelete] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppImageDelete)] [ApiCosts(0)] public async Task DeleteImage(string app) { var response = await InvokeCommandAsync(new RemoveAppImage()); return Ok(response); } /// /// Archive the app. /// /// The name of the app to archive. /// /// 204 => App archived. /// 404 => App not found. /// [HttpDelete] [Route("apps/{app}/")] [ApiPermission(Permissions.AppDelete)] [ApiCosts(0)] public async Task DeleteApp(string app) { await CommandBus.PublishAsync(new ArchiveApp()); return NoContent(); } private Task InvokeCommandAsync(ICommand command) { return InvokeCommandAsync(command, x => { var userOrClientId = HttpContext.User.UserOrClientId()!; var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); return AppDto.FromApp(x, userOrClientId, isFrontend, Resources); }); } private async Task InvokeCommandAsync(ICommand command, Func converter) { var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = converter(result); return response; } private UploadAppImage CreateCommand(IFormFile? file) { if (file == null || Request.Form.Files.Count != 1) { var error = T.Get("validation.onlyOneFile"); throw new ValidationException(error); } return new UploadAppImage { File = file.ToAssetFile() }; } private static FileStream GetTempStream() { var tempFileName = Path.GetTempFileName(); return new FileStream(tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); } } }