432 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			432 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.ComponentModel.DataAnnotations;
 | |
| using DysonNetwork.Develop.Project;
 | |
| using DysonNetwork.Shared.Models;
 | |
| using DysonNetwork.Shared.Proto;
 | |
| using Microsoft.AspNetCore.Authorization;
 | |
| using Microsoft.AspNetCore.Mvc;
 | |
| using NodaTime;
 | |
| 
 | |
| namespace DysonNetwork.Develop.Identity;
 | |
| 
 | |
| [ApiController]
 | |
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/apps")]
 | |
| public class CustomAppController(CustomAppService customApps, DeveloperService ds, DevProjectService projectService)
 | |
|     : ControllerBase
 | |
| {
 | |
|     public record CustomAppRequest(
 | |
|         [MaxLength(1024)] string? Slug,
 | |
|         [MaxLength(1024)] string? Name,
 | |
|         [MaxLength(4096)] string? Description,
 | |
|         string? PictureId,
 | |
|         string? BackgroundId,
 | |
|         Shared.Models.CustomAppStatus? Status,
 | |
|         SnCustomAppLinks? Links,
 | |
|         SnCustomAppOauthConfig? OauthConfig
 | |
|     );
 | |
| 
 | |
|     public record CreateSecretRequest(
 | |
|         [MaxLength(4096)] string? Description,
 | |
|         TimeSpan? ExpiresIn = null,
 | |
|         bool IsOidc = false
 | |
|     );
 | |
| 
 | |
|     public record SecretResponse(
 | |
|         string Id,
 | |
|         string? Secret,
 | |
|         string? Description,
 | |
|         Instant? ExpiresAt,
 | |
|         bool IsOidc,
 | |
|         Instant CreatedAt,
 | |
|         Instant UpdatedAt
 | |
|     );
 | |
| 
 | |
|     [HttpGet]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null) return NotFound();
 | |
| 
 | |
|         var accountId = Guid.Parse(currentUser.Id);
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
 | |
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null) return NotFound();
 | |
| 
 | |
|         var apps = await customApps.GetAppsByProjectAsync(projectId);
 | |
|         return Ok(apps);
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{appId:guid}")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
|         
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null) return NotFound();
 | |
|         
 | |
|         var accountId = Guid.Parse(currentUser.Id);
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
 | |
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null) return NotFound();
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound();
 | |
| 
 | |
|         return Ok(app);
 | |
|     }
 | |
| 
 | |
|     [HttpPost]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> CreateApp(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromBody] CustomAppRequest request)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to create a custom app");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug))
 | |
|             return BadRequest("Name and slug are required");
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var app = await customApps.CreateAppAsync(projectId, request);
 | |
|             if (app == null)
 | |
|                 return BadRequest("Failed to create app");
 | |
| 
 | |
|             return CreatedAtAction(
 | |
|                 nameof(GetApp),
 | |
|                 new { pubName, projectId, appId = app.Id },
 | |
|                 app
 | |
|             );
 | |
|         }
 | |
|         catch (InvalidOperationException ex)
 | |
|         {
 | |
|             return BadRequest(ex.Message);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpPatch("{appId:guid}")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> UpdateApp(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId,
 | |
|         [FromBody] CustomAppRequest request
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to update a custom app");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound();
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             app = await customApps.UpdateAppAsync(app, request);
 | |
|             return Ok(app);
 | |
|         }
 | |
|         catch (InvalidOperationException ex)
 | |
|         {
 | |
|             return BadRequest(ex.Message);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpDelete("{appId:guid}")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> DeleteApp(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to delete a custom app");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound();
 | |
| 
 | |
|         var result = await customApps.DeleteAppAsync(appId);
 | |
|         if (!result)
 | |
|             return NotFound();
 | |
| 
 | |
|         return NoContent();
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{appId:guid}/secrets")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> ListSecrets(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound("App not found");
 | |
| 
 | |
|         var secrets = await customApps.GetAppSecretsAsync(appId);
 | |
|         return Ok(secrets.Select(s => new SecretResponse(
 | |
|             s.Id.ToString(),
 | |
|             null,
 | |
|             s.Description,
 | |
|             s.ExpiredAt,
 | |
|             s.IsOidc,
 | |
|             s.CreatedAt,
 | |
|             s.UpdatedAt
 | |
|         )));
 | |
|     }
 | |
| 
 | |
|     [HttpPost("{appId:guid}/secrets")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> CreateSecret(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId,
 | |
|         [FromBody] CreateSecretRequest request)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to create app secrets");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound("App not found");
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret
 | |
|             {
 | |
|                 AppId = appId,
 | |
|                 Description = request.Description,
 | |
|                 ExpiredAt = request.ExpiresIn.HasValue
 | |
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant()
 | |
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
 | |
|                     : (NodaTime.Instant?)null,
 | |
|                 IsOidc = request.IsOidc
 | |
|             });
 | |
| 
 | |
|             return CreatedAtAction(
 | |
|                 nameof(GetSecret),
 | |
|                 new { pubName, projectId, appId, secretId = secret.Id },
 | |
|                 new SecretResponse(
 | |
|                     secret.Id.ToString(),
 | |
|                     secret.Secret,
 | |
|                     secret.Description,
 | |
|                     secret.ExpiredAt,
 | |
|                     secret.IsOidc,
 | |
|                     secret.CreatedAt,
 | |
|                     secret.UpdatedAt
 | |
|                 )
 | |
|             );
 | |
|         }
 | |
|         catch (InvalidOperationException ex)
 | |
|         {
 | |
|             return BadRequest(ex.Message);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{appId:guid}/secrets/{secretId:guid}")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> GetSecret(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId,
 | |
|         [FromRoute] Guid secretId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound("App not found");
 | |
| 
 | |
|         var secret = await customApps.GetAppSecretAsync(secretId, appId);
 | |
|         if (secret == null)
 | |
|             return NotFound("Secret not found");
 | |
| 
 | |
|         return Ok(new SecretResponse(
 | |
|             secret.Id.ToString(),
 | |
|             null,
 | |
|             secret.Description,
 | |
|             secret.ExpiredAt,
 | |
|             secret.IsOidc,
 | |
|             secret.CreatedAt,
 | |
|             secret.UpdatedAt
 | |
|         ));
 | |
|     }
 | |
| 
 | |
|     [HttpDelete("{appId:guid}/secrets/{secretId:guid}")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> DeleteSecret(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId,
 | |
|         [FromRoute] Guid secretId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to delete app secrets");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound("App not found");
 | |
| 
 | |
|         var secret = await customApps.GetAppSecretAsync(secretId, appId);
 | |
|         if (secret == null)
 | |
|             return NotFound("Secret not found");
 | |
| 
 | |
|         var result = await customApps.DeleteAppSecretAsync(secretId, appId);
 | |
|         if (!result)
 | |
|             return NotFound("Failed to delete secret");
 | |
| 
 | |
|         return NoContent();
 | |
|     }
 | |
| 
 | |
|     [HttpPost("{appId:guid}/secrets/{secretId:guid}/rotate")]
 | |
|     [Authorize]
 | |
|     public async Task<IActionResult> RotateSecret(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid appId,
 | |
|         [FromRoute] Guid secretId,
 | |
|         [FromBody] CreateSecretRequest? request = null)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to rotate app secrets");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var app = await customApps.GetAppAsync(appId, projectId);
 | |
|         if (app == null)
 | |
|             return NotFound("App not found");
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret
 | |
|             {
 | |
|                 Id = secretId,
 | |
|                 AppId = appId,
 | |
|                 Description = request?.Description,
 | |
|                 ExpiredAt = request?.ExpiresIn.HasValue == true
 | |
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant()
 | |
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
 | |
|                     : (NodaTime.Instant?)null,
 | |
|                 IsOidc = request?.IsOidc ?? false
 | |
|             });
 | |
| 
 | |
|             return Ok(new SecretResponse(
 | |
|                 secret.Id.ToString(),
 | |
|                 secret.Secret,
 | |
|                 secret.Description,
 | |
|                 secret.ExpiredAt,
 | |
|                 secret.IsOidc,
 | |
|                 secret.CreatedAt,
 | |
|                 secret.UpdatedAt
 | |
|             ));
 | |
|         }
 | |
|         catch (InvalidOperationException ex)
 | |
|         {
 | |
|             return BadRequest(ex.Message);
 | |
|         }
 | |
|     }
 | |
| } |