Developer projects

This commit is contained in:
2025-08-18 20:49:09 +08:00
parent 29550401fd
commit 665595b8b4
14 changed files with 752 additions and 52 deletions

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Google.Protobuf;
@@ -37,8 +38,11 @@ public class CustomApp : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
public Guid DeveloperId { get; set; }
public Developer Developer { get; set; } = null!;
public Guid ProjectId { get; set; }
public DevProject Project { get; set; } = null!;
[NotMapped]
public Developer Developer => Project.Developer;
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
@@ -72,7 +76,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
RequirePkce = OauthConfig.RequirePkce,
AllowOfflineAccess = OauthConfig.AllowOfflineAccess
},
DeveloperId = DeveloperId.ToString(),
ProjectId = ProjectId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
@@ -92,7 +96,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended,
_ => CustomAppStatus.Developing
};
DeveloperId = string.IsNullOrEmpty(p.DeveloperId) ? Guid.Empty : Guid.Parse(p.DeveloperId);
ProjectId = string.IsNullOrEmpty(p.ProjectId) ? Guid.Empty : Guid.Parse(p.ProjectId);
CreatedAt = p.CreatedAt.ToInstant();
UpdatedAt = p.UpdatedAt.ToInstant();
if (p.Picture.Length > 0) Picture = System.Text.Json.JsonSerializer.Deserialize<CloudFileReferenceObject>(p.Picture.ToStringUtf8());

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -6,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Develop.Identity;
[ApiController]
[Route("/api/developers/{pubName}/apps")]
public class CustomAppController(CustomAppService customApps, DeveloperService ds) : ControllerBase
[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,
@@ -21,21 +22,28 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
);
[HttpGet]
public async Task<IActionResult> ListApps([FromRoute] string pubName)
public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId)
{
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
var apps = await customApps.GetAppsByPublisherAsync(developer.Id);
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null) return NotFound();
var apps = await customApps.GetAppsByProjectAsync(projectId);
return Ok(apps);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetApp([FromRoute] string pubName, Guid id)
[HttpGet("{appId:guid}")]
public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId, [FromRoute] Guid appId)
{
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null) return NotFound();
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
var app = await customApps.GetAppAsync(appId, projectId);
if (app == null)
return NotFound();
@@ -44,23 +52,40 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
[HttpPost]
[Authorize]
public async Task<IActionResult> CreateApp([FromRoute] string pubName, [FromBody] CustomAppRequest request)
public async Task<IActionResult> CreateApp(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromBody] CustomAppRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName);
var accountId = Guid.Parse(currentUser.Id);
if (developer is null || developer.Id != accountId)
return Forbid();
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");
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to create a custom app");
try
{
var app = await customApps.CreateAppAsync(developer, request);
return Ok(app);
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)
{
@@ -68,23 +93,30 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
}
}
[HttpPatch("{id:guid}")]
[HttpPatch("{appId:guid}")]
[Authorize]
public async Task<IActionResult> UpdateApp(
[FromRoute] string pubName,
[FromRoute] Guid id,
[FromRoute] Guid projectId,
[FromRoute] Guid appId,
[FromBody] CustomAppRequest request
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
if (developer is null)
return NotFound("Developer not found");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), 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(id, developerId: developer.Id);
var app = await customApps.GetAppAsync(appId, projectId);
if (app == null)
return NotFound();
@@ -99,28 +131,36 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
}
}
[HttpDelete("{id:guid}")]
[HttpDelete("{appId:guid}")]
[Authorize]
public async Task<IActionResult> DeleteApp(
[FromRoute] string pubName,
[FromRoute] Guid id
[FromRoute] Guid projectId,
[FromRoute] Guid appId
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName);
if (developer is null) return NotFound();
if (developer is null)
return NotFound("Developer not found");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), 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(id, developerId: developer.Id);
var app = await customApps.GetAppAsync(appId, projectId);
if (app == null)
return NotFound();
var result = await customApps.DeleteAppAsync(id);
var result = await customApps.DeleteAppAsync(appId);
if (!result)
return NotFound();
return NoContent();
}
}

View File

@@ -1,3 +1,4 @@
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
@@ -11,10 +12,17 @@ public class CustomAppService(
)
{
public async Task<CustomApp?> CreateAppAsync(
Developer pub,
Guid projectId,
CustomAppController.CustomAppRequest request
)
{
var project = await db.DevProjects
.Include(p => p.Developer)
.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null)
return null;
var app = new CustomApp
{
Slug = request.Slug!,
@@ -23,7 +31,7 @@ public class CustomAppService(
Status = request.Status ?? CustomAppStatus.Developing,
Links = request.Links,
OauthConfig = request.OauthConfig,
DeveloperId = pub.Id
ProjectId = projectId
};
if (request.PictureId is not null)
@@ -74,17 +82,23 @@ public class CustomAppService(
return app;
}
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? developerId = null)
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null)
{
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
if (developerId.HasValue)
query = query.Where(a => a.DeveloperId == developerId.Value);
return await query.FirstOrDefaultAsync();
var query = db.CustomApps.AsQueryable();
if (projectId.HasValue)
{
query = query.Where(a => a.ProjectId == projectId.Value);
}
return await query.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId)
{
return await db.CustomApps.Where(a => a.DeveloperId == publisherId).ToListAsync();
return await db.CustomApps
.Where(a => a.ProjectId == projectId)
.ToListAsync();
}
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Data;
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
@@ -10,6 +11,8 @@ public class Developer
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PublisherId { get; set; }
public List<DevProject> Projects { get; set; } = [];
[NotMapped] public PublisherInfo? Publisher { get; set; }
}

View File

@@ -33,7 +33,8 @@ public class DeveloperController(
// Get custom apps count
var customAppsCount = await db.CustomApps
.Where(a => a.DeveloperId == developer.Id)
.Include(a => a.Project)
.Where(a => a.Project.DeveloperId == developer.Id)
.CountAsync();
var stats = new DeveloperStats