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

@@ -18,6 +18,7 @@ public class AppDatabase(
public DbSet<SnCustomApp> CustomApps { get; set; } = null!; public DbSet<SnCustomApp> CustomApps { get; set; } = null!;
public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!; public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
public DbSet<SnBotAccount> BotAccounts { get; set; } = null!; public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
public DbSet<SnMiniApp> MiniApps { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

View File

@@ -29,4 +29,8 @@
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="MiniApp\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,382 @@
// <auto-generated />
using System;
using DysonNetwork.Develop;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Develop.Migrations
{
[DbContext(typeof(AppDatabase))]
[Migration("20260117175714_AddMiniApp")]
partial class AddMiniApp
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasColumnName("is_active");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid")
.HasColumnName("project_id");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_bot_accounts");
b.HasIndex("ProjectId")
.HasDatabaseName("ix_bot_accounts_project_id");
b.ToTable("bot_accounts", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<SnCloudFileReferenceObject>("Background")
.HasColumnType("jsonb")
.HasColumnName("background");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<SnCustomAppLinks>("Links")
.HasColumnType("jsonb")
.HasColumnName("links");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<SnCustomAppOauthConfig>("OauthConfig")
.HasColumnType("jsonb")
.HasColumnName("oauth_config");
b.Property<SnCloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid")
.HasColumnName("project_id");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<SnVerificationMark>("Verification")
.HasColumnType("jsonb")
.HasColumnName("verification");
b.HasKey("Id")
.HasName("pk_custom_apps");
b.HasIndex("ProjectId")
.HasDatabaseName("ix_custom_apps_project_id");
b.ToTable("custom_apps", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AppId")
.HasColumnType("uuid")
.HasColumnName("app_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<bool>("IsOidc")
.HasColumnType("boolean")
.HasColumnName("is_oidc");
b.Property<string>("Secret")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("secret");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_custom_app_secrets");
b.HasIndex("AppId")
.HasDatabaseName("ix_custom_app_secrets_app_id");
b.ToTable("custom_app_secrets", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Guid>("DeveloperId")
.HasColumnType("uuid")
.HasColumnName("developer_id");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_dev_projects");
b.HasIndex("DeveloperId")
.HasDatabaseName("ix_dev_projects_developer_id");
b.ToTable("dev_projects", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("PublisherId")
.HasColumnType("uuid")
.HasColumnName("publisher_id");
b.HasKey("Id")
.HasName("pk_developers");
b.ToTable("developers", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<MiniAppManifest>("Manifest")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("manifest");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid")
.HasColumnName("project_id");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<int>("Stage")
.HasColumnType("integer")
.HasColumnName("stage");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_mini_apps");
b.HasIndex("ProjectId")
.HasDatabaseName("ix_mini_apps_project_id");
b.ToTable("mini_apps", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_bot_accounts_dev_projects_project_id");
b.Navigation("Project");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_custom_apps_dev_projects_project_id");
b.Navigation("Project");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnCustomApp", "App")
.WithMany("Secrets")
.HasForeignKey("AppId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
b.Navigation("App");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDeveloper", "Developer")
.WithMany("Projects")
.HasForeignKey("DeveloperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_dev_projects_developers_developer_id");
b.Navigation("Developer");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_mini_apps_dev_projects_project_id");
b.Navigation("Project");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{
b.Navigation("Secrets");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
{
b.Navigation("Projects");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Develop.Migrations
{
/// <inheritdoc />
public partial class AddMiniApp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "mini_apps",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
stage = table.Column<int>(type: "integer", nullable: false),
manifest = table.Column<MiniAppManifest>(type: "jsonb", nullable: false),
project_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_mini_apps", x => x.id);
table.ForeignKey(
name: "fk_mini_apps_dev_projects_project_id",
column: x => x.project_id,
principalTable: "dev_projects",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_mini_apps_project_id",
table: "mini_apps",
column: "project_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "mini_apps");
}
}
}

View File

@@ -19,12 +19,12 @@ namespace DysonNetwork.Develop.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.7") .HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -66,7 +66,7 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("bot_accounts", (string)null); b.ToTable("bot_accounts", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -139,7 +139,7 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("custom_apps", (string)null); b.ToTable("custom_apps", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -190,24 +190,7 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("custom_app_secrets", (string)null); b.ToTable("custom_app_secrets", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("PublisherId")
.HasColumnType("uuid")
.HasColumnName("publisher_id");
b.HasKey("Id")
.HasName("pk_developers");
b.ToTable("developers", (string)null);
});
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -257,9 +240,73 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("dev_projects", (string)null); b.ToTable("dev_projects", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
{ {
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("PublisherId")
.HasColumnType("uuid")
.HasColumnName("publisher_id");
b.HasKey("Id")
.HasName("pk_developers");
b.ToTable("developers", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<MiniAppManifest>("Manifest")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("manifest");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid")
.HasColumnName("project_id");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<int>("Stage")
.HasColumnType("integer")
.HasColumnName("stage");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_mini_apps");
b.HasIndex("ProjectId")
.HasDatabaseName("ix_mini_apps_project_id");
b.ToTable("mini_apps", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany() .WithMany()
.HasForeignKey("ProjectId") .HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -269,9 +316,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Project"); b.Navigation("Project");
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{ {
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany() .WithMany()
.HasForeignKey("ProjectId") .HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -281,9 +328,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Project"); b.Navigation("Project");
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
{ {
b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") b.HasOne("DysonNetwork.Shared.Models.SnCustomApp", "App")
.WithMany("Secrets") .WithMany("Secrets")
.HasForeignKey("AppId") .HasForeignKey("AppId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -293,9 +340,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("App"); b.Navigation("App");
}); });
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
{ {
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") b.HasOne("DysonNetwork.Shared.Models.SnDeveloper", "Developer")
.WithMany("Projects") .WithMany("Projects")
.HasForeignKey("DeveloperId") .HasForeignKey("DeveloperId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -305,12 +352,24 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Developer"); b.Navigation("Developer");
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_mini_apps_dev_projects_project_id");
b.Navigation("Project");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
{ {
b.Navigation("Secrets"); b.Navigation("Secrets");
}); });
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
{ {
b.Navigation("Projects"); b.Navigation("Projects");
}); });

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;
}
}

View File

@@ -48,6 +48,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<CustomAppService>(); services.AddScoped<CustomAppService>();
services.AddScoped<DevProjectService>(); services.AddScoped<DevProjectService>();
services.AddScoped<BotAccountService>(); services.AddScoped<BotAccountService>();
services.AddScoped<MiniApp.MiniAppService>();
return services; return services;
} }

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DysonNetwork.Shared.Models;
public enum MiniAppStage
{
Development,
Staging,
Production
}
public class MiniAppManifest
{
public string EntryUrl { get; set; } = null!;
}
public class SnMiniApp : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Slug { get; set; } = null!;
public MiniAppStage Stage { get; set; } = MiniAppStage.Development;
[Column(TypeName = "jsonb")] public MiniAppManifest Manifest { get; set; } = null!;
public Guid ProjectId { get; set; }
public SnDevProject Project { get; set; } = null!;
[NotMapped] public SnDeveloper? Developer { get; set; }
}

View File

@@ -114,7 +114,7 @@ public class PostActionController(
Content = request.Content, Content = request.Content,
Visibility = request.Visibility ?? Shared.Models.PostVisibility.Public, Visibility = request.Visibility ?? Shared.Models.PostVisibility.Public,
PublishedAt = request.PublishedAt, PublishedAt = request.PublishedAt,
Type = request.Type ?? Shared.Models.PostType.Moment, Type = request.Type ?? PostType.Moment,
Metadata = request.Meta, Metadata = request.Meta,
EmbedView = request.EmbedView, EmbedView = request.EmbedView,
Publisher = publisher, Publisher = publisher,