♻️ Refined custom apps
This commit is contained in:
parent
cdeed3c318
commit
c4ea15097e
@ -197,30 +197,6 @@ public class AppDatabase(
|
|||||||
.HasIndex(p => p.SearchVector)
|
.HasIndex(p => p.SearchVector)
|
||||||
.HasMethod("GIN");
|
.HasMethod("GIN");
|
||||||
|
|
||||||
modelBuilder.Entity<CustomApp>()
|
|
||||||
.Property(c => c.RedirectUris)
|
|
||||||
.HasConversion(
|
|
||||||
v => string.Join(",", v),
|
|
||||||
v => v.Split(",", StringSplitOptions.RemoveEmptyEntries).ToArray());
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomApp>()
|
|
||||||
.Property(c => c.PostLogoutRedirectUris)
|
|
||||||
.HasConversion(
|
|
||||||
v => v != null ? string.Join(",", v) : "",
|
|
||||||
v => !string.IsNullOrEmpty(v) ? v.Split(",", StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>());
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomApp>()
|
|
||||||
.Property(c => c.AllowedScopes)
|
|
||||||
.HasConversion(
|
|
||||||
v => v != null ? string.Join(" ", v) : "",
|
|
||||||
v => !string.IsNullOrEmpty(v) ? v.Split(" ", StringSplitOptions.RemoveEmptyEntries) : new[] { "openid", "profile", "email" });
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomApp>()
|
|
||||||
.Property(c => c.AllowedGrantTypes)
|
|
||||||
.HasConversion(
|
|
||||||
v => v != null ? string.Join(" ", v) : "",
|
|
||||||
v => !string.IsNullOrEmpty(v) ? v.Split(" ", StringSplitOptions.RemoveEmptyEntries) : new[] { "authorization_code", "refresh_token" });
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomAppSecret>()
|
modelBuilder.Entity<CustomAppSecret>()
|
||||||
.HasIndex(s => s.Secret)
|
.HasIndex(s => s.Secret)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
@ -158,8 +158,8 @@ public class OidcProviderService(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add scopes as claims if provided
|
// Add scopes as claims if provided
|
||||||
var effectiveScopes = scopes?.ToList() ?? client.AllowedScopes?.ToList() ?? new List<string>();
|
var effectiveScopes = scopes?.ToList() ?? client.OauthConfig!.AllowedScopes?.ToList() ?? [];
|
||||||
if (effectiveScopes.Any())
|
if (effectiveScopes.Count != 0)
|
||||||
{
|
{
|
||||||
tokenDescriptor.Subject.AddClaims(
|
tokenDescriptor.Subject.AddClaims(
|
||||||
effectiveScopes.Select(scope => new Claim("scope", scope)));
|
effectiveScopes.Select(scope => new Claim("scope", scope)));
|
||||||
@ -221,7 +221,7 @@ public class OidcProviderService(
|
|||||||
return string.Equals(secret, hashedSecret, StringComparison.Ordinal);
|
return string.Equals(secret, hashedSecret, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GenerateAuthorizationCodeForExistingSessionAsync(
|
public async Task<string> GenerateAuthorizationCodeForReuseSessionAsync(
|
||||||
Session session,
|
Session session,
|
||||||
Guid clientId,
|
Guid clientId,
|
||||||
string redirectUri,
|
string redirectUri,
|
||||||
|
3993
DysonNetwork.Sphere/Data/Migrations/20250629123136_CustomAppsRefine.Designer.cs
generated
Normal file
3993
DysonNetwork.Sphere/Data/Migrations/20250629123136_CustomAppsRefine.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,182 @@
|
|||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Developer;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class CustomAppsRefine : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allow_offline_access",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allowed_grant_types",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "allowed_scopes",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "client_uri",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "logo_uri",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "post_logout_redirect_uris",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "require_pkce",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "verified_at",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "verified_as",
|
||||||
|
table: "custom_apps",
|
||||||
|
newName: "description");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<CloudFileReferenceObject>(
|
||||||
|
name: "background",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<CustomAppLinks>(
|
||||||
|
name: "links",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<CustomAppOauthConfig>(
|
||||||
|
name: "oauth_config",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<CloudFileReferenceObject>(
|
||||||
|
name: "picture",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<VerificationMark>(
|
||||||
|
name: "verification",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "background",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "links",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "oauth_config",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "picture",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "verification",
|
||||||
|
table: "custom_apps");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "description",
|
||||||
|
table: "custom_apps",
|
||||||
|
newName: "verified_as");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "allow_offline_access",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "allowed_grant_types",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "allowed_scopes",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "client_uri",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "logo_uri",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "post_logout_redirect_uris",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "require_pkce",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Instant>(
|
||||||
|
name: "verified_at",
|
||||||
|
table: "custom_apps",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Developer;
|
namespace DysonNetwork.Sphere.Developer;
|
||||||
@ -12,17 +15,38 @@ public enum CustomAppStatus
|
|||||||
Suspended
|
Suspended
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomApp : ModelBase
|
public class CustomApp : ModelBase, IIdentifiedResource
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
||||||
[MaxLength(1024)] public string Name { get; set; } = null!;
|
[MaxLength(1024)] public string Name { get; set; } = null!;
|
||||||
|
[MaxLength(4096)] public string? Description { get; set; }
|
||||||
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
|
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
|
||||||
public Instant? VerifiedAt { get; set; }
|
|
||||||
[MaxLength(4096)] public string? VerifiedAs { get; set; }
|
|
||||||
|
|
||||||
// OIDC/OAuth specific properties
|
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||||
[MaxLength(4096)] public string? LogoUri { get; set; }
|
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||||
|
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
|
||||||
|
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||||
|
|
||||||
|
public Guid PublisherId { get; set; }
|
||||||
|
public Publisher.Publisher Developer { get; set; } = null!;
|
||||||
|
|
||||||
|
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomAppLinks : ModelBase
|
||||||
|
{
|
||||||
|
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||||
|
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||||
|
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomAppOauthConfig : ModelBase
|
||||||
|
{
|
||||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||||
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
|
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
|
||||||
@ -30,11 +54,6 @@ public class CustomApp : ModelBase
|
|||||||
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
|
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
|
||||||
public bool RequirePkce { get; set; } = true;
|
public bool RequirePkce { get; set; } = true;
|
||||||
public bool AllowOfflineAccess { get; set; } = false;
|
public bool AllowOfflineAccess { get; set; } = false;
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
|
||||||
|
|
||||||
public Guid PublisherId { get; set; }
|
|
||||||
public Publisher.Publisher Developer { get; set; } = null!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomAppSecret : ModelBase
|
public class CustomAppSecret : ModelBase
|
||||||
|
@ -1,63 +1,129 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Developer;
|
namespace DysonNetwork.Sphere.Developer;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/developers/apps")]
|
[Route("/developers/{pubName}/apps")]
|
||||||
public class CustomAppController(CustomAppService customAppService, PublisherService ps) : ControllerBase
|
public class CustomAppController(CustomAppService customApps, PublisherService ps) : ControllerBase
|
||||||
{
|
{
|
||||||
public record CreateAppRequest(Guid PublisherId, string Name, string Slug);
|
public record CustomAppRequest(
|
||||||
public record UpdateAppRequest(string Name, string Slug);
|
[MaxLength(1024)] string? Slug,
|
||||||
|
[MaxLength(1024)] string? Name,
|
||||||
|
[MaxLength(4096)] string? Description,
|
||||||
|
string? PictureId,
|
||||||
|
string? BackgroundId,
|
||||||
|
CustomAppStatus? Status,
|
||||||
|
CustomAppLinks? Links,
|
||||||
|
CustomAppOauthConfig? OauthConfig
|
||||||
|
);
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ListApps([FromQuery] Guid publisherId)
|
public async Task<IActionResult> ListApps([FromRoute] string pubName)
|
||||||
{
|
{
|
||||||
var apps = await customAppService.GetAppsByPublisherAsync(publisherId);
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
|
if (publisher is null) return NotFound();
|
||||||
|
var apps = await customApps.GetAppsByPublisherAsync(publisher.Id);
|
||||||
return Ok(apps);
|
return Ok(apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> GetApp(Guid id)
|
public async Task<IActionResult> GetApp([FromRoute] string pubName, Guid id)
|
||||||
{
|
{
|
||||||
var app = await customAppService.GetAppAsync(id);
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
|
if (publisher is null) return NotFound();
|
||||||
|
|
||||||
|
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
return Ok(app);
|
return Ok(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateApp([FromBody] CreateAppRequest request)
|
[Authorize]
|
||||||
|
public async Task<IActionResult> CreateApp([FromRoute] string pubName, [FromBody] CustomAppRequest request)
|
||||||
{
|
{
|
||||||
var app = await customAppService.CreateAppAsync(request.PublisherId, request.Name, request.Slug);
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
if (app == null)
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug))
|
||||||
|
return BadRequest("Name and slug are required");
|
||||||
|
|
||||||
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
|
if (publisher is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
|
||||||
|
return StatusCode(403, "You must be an editor of the publisher to create a custom app");
|
||||||
|
if (!await ps.HasFeature(publisher.Id, PublisherFeatureFlag.Develop))
|
||||||
|
return StatusCode(403, "Publisher must be a developer to create a custom app");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid publisher ID or missing developer feature flag");
|
var app = await customApps.CreateAppAsync(publisher, request);
|
||||||
|
return Ok(app);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
return CreatedAtAction(nameof(GetApp), new { id = app.Id }, app);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id:guid}")]
|
[HttpPatch("{id:guid}")]
|
||||||
public async Task<IActionResult> UpdateApp(Guid id, [FromBody] UpdateAppRequest request)
|
[Authorize]
|
||||||
|
public async Task<IActionResult> UpdateApp(
|
||||||
|
[FromRoute] string pubName,
|
||||||
|
[FromRoute] Guid id,
|
||||||
|
[FromBody] CustomAppRequest request
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var app = await customAppService.UpdateAppAsync(id, request.Name, request.Slug);
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
|
if (publisher is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
|
||||||
|
return StatusCode(403, "You must be an editor of the publisher to update a custom app");
|
||||||
|
|
||||||
|
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
app = await customApps.UpdateAppAsync(app, request);
|
||||||
return Ok(app);
|
return Ok(app);
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
public async Task<IActionResult> DeleteApp(Guid id)
|
[Authorize]
|
||||||
{
|
public async Task<IActionResult> DeleteApp(
|
||||||
var result = await customAppService.DeleteAppAsync(id);
|
[FromRoute] string pubName,
|
||||||
if (!result)
|
[FromRoute] Guid id
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var publisher = await ps.GetPublisherByName(pubName);
|
||||||
|
if (publisher is null) return NotFound();
|
||||||
|
|
||||||
|
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
|
||||||
|
return StatusCode(403, "You must be an editor of the publisher to delete a custom app");
|
||||||
|
|
||||||
|
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||||
|
if (app == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var result = await customApps.DeleteAppAsync(id);
|
||||||
|
if (!result)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,39 +1,71 @@
|
|||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Developer;
|
namespace DysonNetwork.Sphere.Developer;
|
||||||
|
|
||||||
public class CustomAppService(AppDatabase db, PublisherService ps)
|
public class CustomAppService(AppDatabase db, FileReferenceService fileRefService)
|
||||||
{
|
{
|
||||||
public async Task<CustomApp?> CreateAppAsync(Guid publisherId, string name, string slug)
|
public async Task<CustomApp?> CreateAppAsync(
|
||||||
|
Publisher.Publisher pub,
|
||||||
|
CustomAppController.CustomAppRequest request
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var publisher = await db.Publishers.FirstOrDefaultAsync(p => p.Id == publisherId);
|
|
||||||
if (publisher == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await ps.HasFeature(publisherId, "developer"))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var app = new CustomApp
|
var app = new CustomApp
|
||||||
{
|
{
|
||||||
Name = name,
|
Slug = request.Slug!,
|
||||||
Slug = slug,
|
Name = request.Name!,
|
||||||
PublisherId = publisher.Id
|
Description = request.Description,
|
||||||
|
Status = request.Status ?? CustomAppStatus.Developing,
|
||||||
|
Links = request.Links,
|
||||||
|
OauthConfig = request.OauthConfig,
|
||||||
|
PublisherId = pub.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (request.PictureId is not null)
|
||||||
|
{
|
||||||
|
var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
||||||
|
if (picture is null)
|
||||||
|
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
app.Picture = picture.ToReferenceObject();
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefService.CreateReferenceAsync(
|
||||||
|
picture.Id,
|
||||||
|
"custom-apps.picture",
|
||||||
|
app.ResourceIdentifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.BackgroundId is not null)
|
||||||
|
{
|
||||||
|
var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
||||||
|
if (background is null)
|
||||||
|
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
app.Background = background.ToReferenceObject();
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefService.CreateReferenceAsync(
|
||||||
|
background.Id,
|
||||||
|
"custom-apps.background",
|
||||||
|
app.ResourceIdentifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
db.CustomApps.Add(app);
|
db.CustomApps.Add(app);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CustomApp?> GetAppAsync(Guid id)
|
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? publisherId = null)
|
||||||
{
|
{
|
||||||
return await db.CustomApps.FindAsync(id);
|
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
|
||||||
|
if (publisherId.HasValue)
|
||||||
|
query = query.Where(a => a.PublisherId == publisherId.Value);
|
||||||
|
return await query.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
|
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
|
||||||
@ -41,17 +73,60 @@ public class CustomAppService(AppDatabase db, PublisherService ps)
|
|||||||
return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync();
|
return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CustomApp?> UpdateAppAsync(Guid id, string name, string slug)
|
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
|
||||||
{
|
{
|
||||||
var app = await db.CustomApps.FindAsync(id);
|
if (request.Slug is not null)
|
||||||
if (app == null)
|
app.Slug = request.Slug;
|
||||||
|
if (request.Name is not null)
|
||||||
|
app.Name = request.Name;
|
||||||
|
if (request.Description is not null)
|
||||||
|
app.Description = request.Description;
|
||||||
|
if (request.Status is not null)
|
||||||
|
app.Status = request.Status.Value;
|
||||||
|
if (request.Links is not null)
|
||||||
|
app.Links = request.Links;
|
||||||
|
if (request.OauthConfig is not null)
|
||||||
|
app.OauthConfig = request.OauthConfig;
|
||||||
|
|
||||||
|
if (request.PictureId is not null)
|
||||||
{
|
{
|
||||||
return null;
|
var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
||||||
|
if (picture is null)
|
||||||
|
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
if (app.Picture is not null)
|
||||||
|
await fileRefService.DeleteResourceReferencesAsync(app.ResourceIdentifier, "custom-apps.picture");
|
||||||
|
|
||||||
|
app.Picture = picture.ToReferenceObject();
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefService.CreateReferenceAsync(
|
||||||
|
picture.Id,
|
||||||
|
"custom-apps.picture",
|
||||||
|
app.ResourceIdentifier
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Name = name;
|
if (request.BackgroundId is not null)
|
||||||
app.Slug = slug;
|
{
|
||||||
|
var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
||||||
|
if (background is null)
|
||||||
|
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
|
if (app.Background is not null)
|
||||||
|
await fileRefService.DeleteResourceReferencesAsync(app.ResourceIdentifier, "custom-apps.background");
|
||||||
|
|
||||||
|
app.Background = background.ToReferenceObject();
|
||||||
|
|
||||||
|
// Create a new reference
|
||||||
|
await fileRefService.CreateReferenceAsync(
|
||||||
|
background.Id,
|
||||||
|
"custom-apps.background",
|
||||||
|
app.ResourceIdentifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Update(app);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@ -68,6 +143,8 @@ public class CustomAppService(AppDatabase db, PublisherService ps)
|
|||||||
db.CustomApps.Remove(app);
|
db.CustomApps.Remove(app);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await fileRefService.DeleteResourceReferencesAsync(app.ResourceIdentifier);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ using DysonNetwork.Sphere;
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
using DysonNetwork.Sphere.Connection.WebReader;
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
using DysonNetwork.Sphere.Developer;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Sphere.Wallet;
|
using DysonNetwork.Sphere.Wallet;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -1507,25 +1508,9 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
b.Property<bool>("AllowOfflineAccess")
|
b.Property<CloudFileReferenceObject>("Background")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("allow_offline_access");
|
.HasColumnName("background");
|
||||||
|
|
||||||
b.Property<string>("AllowedGrantTypes")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)")
|
|
||||||
.HasColumnName("allowed_grant_types");
|
|
||||||
|
|
||||||
b.Property<string>("AllowedScopes")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)")
|
|
||||||
.HasColumnName("allowed_scopes");
|
|
||||||
|
|
||||||
b.Property<string>("ClientUri")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)")
|
|
||||||
.HasColumnName("client_uri");
|
|
||||||
|
|
||||||
b.Property<Instant>("CreatedAt")
|
b.Property<Instant>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
@ -1535,10 +1520,14 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("deleted_at");
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
b.Property<string>("LogoUri")
|
b.Property<string>("Description")
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnType("character varying(4096)")
|
||||||
.HasColumnName("logo_uri");
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<CustomAppLinks>("Links")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("links");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@ -1546,25 +1535,18 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("character varying(1024)")
|
.HasColumnType("character varying(1024)")
|
||||||
.HasColumnName("name");
|
.HasColumnName("name");
|
||||||
|
|
||||||
b.Property<string>("PostLogoutRedirectUris")
|
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||||
.HasMaxLength(4096)
|
.HasColumnType("jsonb")
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnName("oauth_config");
|
||||||
.HasColumnName("post_logout_redirect_uris");
|
|
||||||
|
b.Property<CloudFileReferenceObject>("Picture")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("picture");
|
||||||
|
|
||||||
b.Property<Guid>("PublisherId")
|
b.Property<Guid>("PublisherId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("publisher_id");
|
.HasColumnName("publisher_id");
|
||||||
|
|
||||||
b.Property<string>("RedirectUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)")
|
|
||||||
.HasColumnName("redirect_uris");
|
|
||||||
|
|
||||||
b.Property<bool>("RequirePkce")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("require_pkce");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
b.Property<string>("Slug")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
@ -1579,14 +1561,9 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
b.Property<string>("VerifiedAs")
|
b.Property<VerificationMark>("Verification")
|
||||||
.HasMaxLength(4096)
|
.HasColumnType("jsonb")
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnName("verification");
|
||||||
.HasColumnName("verified_as");
|
|
||||||
|
|
||||||
b.Property<Instant?>("VerifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("verified_at");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_custom_apps");
|
.HasName("pk_custom_apps");
|
||||||
@ -3618,7 +3595,7 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher")
|
b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher")
|
||||||
.WithMany()
|
.WithMany("Features")
|
||||||
.HasForeignKey("PublisherId")
|
.HasForeignKey("PublisherId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@ -3980,6 +3957,8 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
{
|
{
|
||||||
b.Navigation("Collections");
|
b.Navigation("Collections");
|
||||||
|
|
||||||
|
b.Navigation("Features");
|
||||||
|
|
||||||
b.Navigation("Members");
|
b.Navigation("Members");
|
||||||
|
|
||||||
b.Navigation("Posts");
|
b.Navigation("Posts");
|
||||||
|
@ -8,7 +8,7 @@ using DysonNetwork.Sphere.Developer;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Pages.Auth;
|
namespace DysonNetwork.Sphere.Pages.Auth;
|
||||||
|
|
||||||
public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
public class AuthorizeModel(OidcProviderService oidcService, IConfiguration configuration) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; }
|
[BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; }
|
||||||
|
|
||||||
@ -71,10 +71,17 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
|||||||
return NotFound("Client not found");
|
return NotFound("Client not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var config = client.OauthConfig;
|
||||||
|
if (config is null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("client_id", "Client was not available for use OAuth / OIDC");
|
||||||
|
return BadRequest("Client was not enabled for OAuth / OIDC");
|
||||||
|
}
|
||||||
|
|
||||||
// Validate redirect URI for non-Developing apps
|
// Validate redirect URI for non-Developing apps
|
||||||
if (client.Status != CustomAppStatus.Developing)
|
if (client.Status != CustomAppStatus.Developing)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(RedirectUri) && !(client.RedirectUris?.Contains(RedirectUri) ?? false))
|
if (!string.IsNullOrEmpty(RedirectUri) && !(config.RedirectUris?.Contains(RedirectUri) ?? false))
|
||||||
{
|
{
|
||||||
return BadRequest(new ErrorResponse
|
return BadRequest(new ErrorResponse
|
||||||
{
|
{
|
||||||
@ -93,9 +100,10 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show authorization page
|
// Show authorization page
|
||||||
|
var baseUrl = configuration["BaseUrl"];
|
||||||
AppName = client.Name;
|
AppName = client.Name;
|
||||||
AppLogo = client.LogoUri;
|
AppLogo = client.Picture is not null ? $"{baseUrl}/files/{client.Picture.Id}" : null;
|
||||||
AppUri = client.ClientUri;
|
AppUri = config.ClientUri;
|
||||||
RequestedScopes = (Scope ?? "openid profile").Split(' ').Distinct().ToArray();
|
RequestedScopes = (Scope ?? "openid profile").Split(' ').Distinct().ToArray();
|
||||||
|
|
||||||
return Page();
|
return Page();
|
||||||
@ -114,7 +122,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
|||||||
if (existingSession != null)
|
if (existingSession != null)
|
||||||
{
|
{
|
||||||
// Reuse existing session
|
// Reuse existing session
|
||||||
authCode = await oidcService.GenerateAuthorizationCodeForExistingSessionAsync(
|
authCode = await oidcService.GenerateAuthorizationCodeForReuseSessionAsync(
|
||||||
session: existingSession,
|
session: existingSession,
|
||||||
clientId: ClientId,
|
clientId: ClientId,
|
||||||
redirectUri: RedirectUri,
|
redirectUri: RedirectUri,
|
||||||
@ -126,7 +134,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Create new session (existing flow)
|
// Create a new session (existing flow)
|
||||||
authCode = await oidcService.GenerateAuthorizationCodeAsync(
|
authCode = await oidcService.GenerateAuthorizationCodeAsync(
|
||||||
clientId: ClientId,
|
clientId: ClientId,
|
||||||
userId: currentUser.Id,
|
userId: currentUser.Id,
|
||||||
|
@ -8,6 +8,13 @@ namespace DysonNetwork.Sphere.Publisher;
|
|||||||
|
|
||||||
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
||||||
{
|
{
|
||||||
|
public async Task<Publisher?> GetPublisherByName(string name)
|
||||||
|
{
|
||||||
|
return await db.Publishers
|
||||||
|
.Where(e => e.Name == name)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
||||||
|
|
||||||
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
||||||
@ -336,7 +343,7 @@ public class PublisherService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
f.PublisherId == publisherId && f.Flag == flag &&
|
f.PublisherId == publisherId && f.Flag == flag &&
|
||||||
(f.ExpiredAt == null || f.ExpiredAt > now)
|
(f.ExpiredAt == null || f.ExpiredAt > now)
|
||||||
);
|
);
|
||||||
if (featureFlag is not null) isEnabled = true;
|
isEnabled = featureFlag is not null;
|
||||||
|
|
||||||
await cache.SetAsync(cacheKey, isEnabled!.Value, TimeSpan.FromMinutes(5));
|
await cache.SetAsync(cacheKey, isEnabled!.Value, TimeSpan.FromMinutes(5));
|
||||||
return isEnabled.Value;
|
return isEnabled.Value;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user