Mini apps in develop

This commit is contained in:
2026-01-18 01:57:24 +08:00
parent b7aac30384
commit a3c1d74501
11 changed files with 863 additions and 33 deletions

View File

@@ -0,0 +1,185 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Develop.MiniApp;
[ApiController]
[Route("/api/developers/{pubName}/projects/{projectId:guid}/miniapps")]
public class MiniAppController(MiniAppService miniAppService, Identity.DeveloperService ds, DevProjectService projectService)
: ControllerBase
{
public record MiniAppRequest(
[MaxLength(1024)] string? Slug,
MiniAppStage? Stage,
MiniAppManifest? Manifest
);
public record CreateMiniAppRequest(
[Required]
[MinLength(2)]
[MaxLength(1024)]
[RegularExpression(@"^[A-Za-z0-9_-]+$",
ErrorMessage = "Slug can only contain letters, numbers, underscores, and hyphens.")]
string Slug,
MiniAppStage Stage = MiniAppStage.Development,
[Required] MiniAppManifest Manifest = null!
);
[HttpGet]
[Authorize]
public async Task<IActionResult> ListMiniApps([FromRoute] string pubName, [FromRoute] Guid projectId)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound("Developer not found");
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 mini apps");
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null) return NotFound("Project not found or you don't have access");
var miniApps = await miniAppService.GetMiniAppsByProjectAsync(projectId);
return Ok(miniApps);
}
[HttpGet("{miniAppId:guid}")]
[Authorize]
public async Task<IActionResult> GetMiniApp([FromRoute] string pubName, [FromRoute] Guid projectId,
[FromRoute] Guid miniAppId)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound("Developer not found");
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 view mini app details");
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null) return NotFound("Project not found or you don't have access");
var miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
if (miniApp == null || miniApp.ProjectId != projectId)
return NotFound("Mini app not found");
return Ok(miniApp);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> CreateMiniApp(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromBody] CreateMiniAppRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.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 mini app");
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null)
return NotFound("Project not found or you don't have access");
try
{
var miniApp = await miniAppService.CreateMiniAppAsync(projectId, request.Slug, request.Stage, request.Manifest);
return CreatedAtAction(
nameof(GetMiniApp),
new { pubName, projectId, miniAppId = miniApp.Id },
miniApp
);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPatch("{miniAppId:guid}")]
[Authorize]
public async Task<IActionResult> UpdateMiniApp(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid miniAppId,
[FromBody] MiniAppRequest request
)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.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 mini 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 miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
if (miniApp == null || miniApp.ProjectId != projectId)
return NotFound("Mini app not found");
try
{
miniApp = await miniAppService.UpdateMiniAppAsync(miniApp, request.Slug, request.Stage, request.Manifest);
return Ok(miniApp);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("{miniAppId:guid}")]
[Authorize]
public async Task<IActionResult> DeleteMiniApp(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid miniAppId
)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.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 mini 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 miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
if (miniApp == null || miniApp.ProjectId != projectId)
return NotFound("Mini app not found");
var result = await miniAppService.DeleteMiniAppAsync(miniAppId);
if (!result)
return NotFound("Failed to delete mini app");
return NoContent();
}
}

View File

@@ -0,0 +1,22 @@
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Develop.MiniApp;
[ApiController]
[Route("api/miniapps")]
public class MiniAppPublicController(MiniAppService miniAppService, Identity.DeveloperService developerService) : ControllerBase
{
[HttpGet("{slug}")]
public async Task<ActionResult<SnMiniApp>> GetMiniAppBySlug([FromRoute] string slug)
{
var miniApp = await miniAppService.GetMiniAppBySlugAsync(slug);
if (miniApp is null) return NotFound("Mini app not found");
var developer = await developerService.GetDeveloperById(miniApp.Project.DeveloperId);
if (developer is null) return NotFound("Developer not found");
miniApp.Developer = await developerService.LoadDeveloperPublisher(developer);
return Ok(miniApp);
}
}

View File

@@ -0,0 +1,92 @@
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop.MiniApp;
public class MiniAppService(AppDatabase db)
{
public async Task<SnMiniApp?> GetMiniAppByIdAsync(Guid id)
{
return await db.MiniApps
.Include(m => m.Project)
.FirstOrDefaultAsync(m => m.Id == id);
}
public async Task<SnMiniApp?> GetMiniAppBySlugAsync(string slug)
{
return await db.MiniApps
.Include(m => m.Project)
.FirstOrDefaultAsync(m => m.Slug == slug);
}
public async Task<List<SnMiniApp>> GetMiniAppsByProjectAsync(Guid projectId)
{
return await db.MiniApps
.Where(m => m.ProjectId == projectId)
.ToListAsync();
}
public async Task<SnMiniApp> CreateMiniAppAsync(Guid projectId, string slug, MiniAppStage stage, MiniAppManifest manifest)
{
var project = await db.DevProjects.FindAsync(projectId);
if (project == null)
throw new ArgumentException("Project not found");
// Check if a mini app with this slug already exists globally
var existingMiniApp = await db.MiniApps
.FirstOrDefaultAsync(m => m.Slug == slug);
if (existingMiniApp != null)
throw new InvalidOperationException("A mini app with this slug already exists.");
var miniApp = new SnMiniApp
{
Id = Guid.NewGuid(),
Slug = slug,
Stage = stage,
Manifest = manifest,
ProjectId = projectId,
Project = project
};
db.MiniApps.Add(miniApp);
await db.SaveChangesAsync();
return miniApp;
}
public async Task<SnMiniApp> UpdateMiniAppAsync(SnMiniApp miniApp, string? slug, MiniAppStage? stage, MiniAppManifest? manifest)
{
if (slug != null && slug != miniApp.Slug)
{
// Check if another mini app with this slug already exists globally
var existingMiniApp = await db.MiniApps
.FirstOrDefaultAsync(m => m.Slug == slug && m.Id != miniApp.Id);
if (existingMiniApp != null)
throw new InvalidOperationException("A mini app with this slug already exists.");
miniApp.Slug = slug;
}
if (stage.HasValue) miniApp.Stage = stage.Value;
if (manifest != null) miniApp.Manifest = manifest;
db.Update(miniApp);
await db.SaveChangesAsync();
return miniApp;
}
public async Task<bool> DeleteMiniAppAsync(Guid id)
{
var miniApp = await db.MiniApps.FindAsync(id);
if (miniApp == null)
return false;
db.MiniApps.Remove(miniApp);
await db.SaveChangesAsync();
return true;
}
}