// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; 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 NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. /// [ApiExplorerSettings(GroupName = nameof(Apps))] public sealed class AppsController : ApiController { private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAppProvider appProvider; private readonly IAppPlansProvider appPlansProvider; public AppsController(ICommandBus commandBus, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator, IAppProvider appProvider, IAppPlansProvider appPlansProvider) : base(commandBus) { this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; this.appProvider = appProvider; this.appPlansProvider = appPlansProvider; } /// /// 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[]), 200)] [ApiPermission] [ApiCosts(0)] public async Task GetApps() { var userOrClientId = HttpContext.User.UserOrClientId(); var userPermissions = HttpContext.Permissions(); var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); var response = Deferred.Response(() => { return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); }); Response.Headers[HeaderNames.ETag] = apps.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. /// 404 => App not found. /// [HttpPut] [Route("apps/{app}/")] [ProducesResponseType(typeof(AppDto), 200)] [ApiPermission(Permissions.AppUpdateGeneral)] [ApiCosts(0)] public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) { var response = await InvokeCommandAsync(request.ToCommand()); return Ok(response); } /// /// Get the app image. /// /// The name of the app to update. /// The file to upload. /// /// 200 => App image uploaded. /// 404 => App not found. /// [HttpPost] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), 201)] [ApiPermission(Permissions.AppUpdateImage)] [ApiCosts(0)] public async Task UploadImage(string app, [OpenApiIgnore] List 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), 200)] [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 handler = new Func(async bodyStream => { var assetId = App.Id.ToString(); var assetResizedId = $"{assetId}_{etag}_Resized"; try { await assetStore.DownloadAsync(assetResizedId, bodyStream); } catch (AssetNotFoundException) { using (Profiler.Trace("Resize")) { using (var sourceStream = GetTempStream()) { using (var destinationStream = GetTempStream()) { using (Profiler.Trace("ResizeDownload")) { await assetStore.DownloadAsync(assetId, sourceStream); sourceStream.Position = 0; } using (Profiler.Trace("ResizeImage")) { await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, 150, 150, "Crop"); destinationStream.Position = 0; } using (Profiler.Trace("ResizeUpload")) { await assetStore.UploadAsync(assetResizedId, destinationStream); destinationStream.Position = 0; } await destinationStream.CopyToAsync(bodyStream); } } } } }); return new FileCallbackResult(App.Image.MimeType, null, true, handler); } /// /// 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), 201)] [ApiPermission(Permissions.AppUpdate)] [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 async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); var userOrClientId = HttpContext.User.UserOrClientId(); var userPermissions = HttpContext.Permissions(); var result = context.Result(); var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); return response; } private static UploadAppImage CreateCommand(IReadOnlyList file) { if (file.Count != 1) { var error = new ValidationError($"Can only upload one file, found {file.Count} files."); throw new ValidationException("Cannot create asset.", error); } return new UploadAppImage { File = file[0].OpenReadStream, Image = new AppImage(file[0].ContentType) }; } 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); } } }