✨ Complete the develop service
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop;
|
||||
@@ -7,6 +8,11 @@ public class AppDatabase(
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<Developer> Developers { get; set; }
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; }
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
|
@@ -31,8 +31,8 @@ public class CustomApp : ModelBase, IIdentifiedResource
|
||||
|
||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public Publisher.Publisher Developer { get; set; } = null!;
|
||||
public Guid DeveloperId { get; set; }
|
||||
public Developer Developer { get; set; } = null!;
|
||||
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/apps")]
|
||||
public class CustomAppController(CustomAppService customApps, PublisherService ps) : ControllerBase
|
||||
public class CustomAppController(CustomAppService customApps, DeveloperService ds) : ControllerBase
|
||||
{
|
||||
public record CustomAppRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
@@ -23,19 +23,19 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListApps([FromRoute] string pubName)
|
||||
{
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
var apps = await customApps.GetAppsByPublisherAsync(publisher.Id);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
var apps = await customApps.GetAppsByPublisherAsync(developer.Id);
|
||||
return Ok(apps);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetApp([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
@@ -51,17 +51,15 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
|
||||
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();
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(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");
|
||||
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(publisher, request);
|
||||
var app = await customApps.CreateAppAsync(developer, request);
|
||||
return Ok(app);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
@@ -80,13 +78,13 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to update a custom app");
|
||||
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 app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
@@ -110,13 +108,13 @@ public class CustomAppController(CustomAppService customApps, PublisherService p
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to delete a custom app");
|
||||
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 app = await customApps.GetAppAsync(id, publisherId: publisher.Id);
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
@@ -10,7 +11,7 @@ public class CustomAppService(
|
||||
)
|
||||
{
|
||||
public async Task<CustomApp?> CreateAppAsync(
|
||||
Publisher.Publisher pub,
|
||||
Developer pub,
|
||||
CustomAppController.CustomAppRequest request
|
||||
)
|
||||
{
|
||||
@@ -22,7 +23,7 @@ public class CustomAppService(
|
||||
Status = request.Status ?? CustomAppStatus.Developing,
|
||||
Links = request.Links,
|
||||
OauthConfig = request.OauthConfig,
|
||||
PublisherId = pub.Id
|
||||
DeveloperId = pub.Id
|
||||
};
|
||||
|
||||
if (request.PictureId is not null)
|
||||
@@ -73,17 +74,17 @@ public class CustomAppService(
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? publisherId = null)
|
||||
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? developerId = null)
|
||||
{
|
||||
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
|
||||
if (publisherId.HasValue)
|
||||
query = query.Where(a => a.PublisherId == publisherId.Value);
|
||||
if (developerId.HasValue)
|
||||
query = query.Where(a => a.DeveloperId == developerId.Value);
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
|
||||
{
|
||||
return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync();
|
||||
return await db.CustomApps.Where(a => a.DeveloperId == publisherId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
|
||||
|
75
DysonNetwork.Develop/Identity/Developer.cs
Normal file
75
DysonNetwork.Develop/Identity/Developer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class Developer
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PublisherId { get; set; }
|
||||
|
||||
[NotMapped] public PublisherInfo? Publisher { get; set; }
|
||||
}
|
||||
|
||||
public class PublisherInfo
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public PublisherType Type { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Nick { get; set; } = string.Empty;
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public CloudFileReferenceObject? Picture { get; set; }
|
||||
public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
public VerificationMark? Verification { get; set; }
|
||||
public Guid? AccountId { get; set; }
|
||||
public Guid? RealmId { get; set; }
|
||||
|
||||
public static PublisherInfo FromProto(Publisher proto)
|
||||
{
|
||||
var info = new PublisherInfo
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Type = proto.Type == PublisherType.PubIndividual
|
||||
? PublisherType.PubIndividual
|
||||
: PublisherType.PubOrganizational,
|
||||
Name = proto.Name,
|
||||
Nick = proto.Nick,
|
||||
Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio,
|
||||
Verification = proto.VerificationMark is not null
|
||||
? VerificationMark.FromProtoValue(proto.VerificationMark)
|
||||
: null,
|
||||
AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId),
|
||||
RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId)
|
||||
};
|
||||
|
||||
if (proto.Picture != null)
|
||||
{
|
||||
info.Picture = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Picture.Id,
|
||||
Name = proto.Picture.Name,
|
||||
MimeType = proto.Picture.MimeType,
|
||||
Hash = proto.Picture.Hash,
|
||||
Size = proto.Picture.Size
|
||||
};
|
||||
}
|
||||
|
||||
if (proto.Background != null)
|
||||
{
|
||||
info.Background = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Background.Id,
|
||||
Name = proto.Background.Name,
|
||||
MimeType = proto.Background.MimeType,
|
||||
Hash = proto.Background.Hash,
|
||||
Size = (long)proto.Background.Size
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
@@ -11,42 +12,28 @@ namespace DysonNetwork.Develop.Identity;
|
||||
public class DeveloperController(
|
||||
AppDatabase db,
|
||||
PublisherService.PublisherServiceClient ps,
|
||||
ActionLogService.ActionLogServiceClient als
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
DeveloperService ds
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Publisher.Publisher>> GetDeveloper(string name)
|
||||
public async Task<ActionResult<Developer>> GetDeveloper(string name)
|
||||
{
|
||||
var publisher = await db.Publishers
|
||||
.Where(e => e.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
return Ok(publisher);
|
||||
var developer = await ds.GetDeveloperByName(name);
|
||||
if (developer is null) return NotFound();
|
||||
return Ok(developer);
|
||||
}
|
||||
|
||||
[HttpGet("{name}/stats")]
|
||||
public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name)
|
||||
{
|
||||
var publisher = await db.Publishers
|
||||
.Where(p => p.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
// Check if publisher has developer feature
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var hasDeveloperFeature = await db.PublisherFeatures
|
||||
.Where(f => f.PublisherId == publisher.Id)
|
||||
.Where(f => f.Flag == PublisherFeatureFlag.Develop)
|
||||
.Where(f => f.ExpiredAt == null || f.ExpiredAt > now)
|
||||
.AnyAsync();
|
||||
|
||||
if (!hasDeveloperFeature) return NotFound("Not a developer account");
|
||||
var developer = await ds.GetDeveloperByName(name);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
// Get custom apps count
|
||||
var customAppsCount = await db.CustomApps
|
||||
.Where(a => a.PublisherId == publisher.Id)
|
||||
.Where(a => a.DeveloperId == developer.Id)
|
||||
.CountAsync();
|
||||
|
||||
var stats = new DeveloperStats
|
||||
@@ -59,75 +46,63 @@ public class DeveloperController(
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Publisher.Publisher>>> ListJoinedDevelopers()
|
||||
public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var members = await db.PublisherMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt != null)
|
||||
.Include(e => e.Publisher)
|
||||
.ToListAsync();
|
||||
var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
|
||||
var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
|
||||
|
||||
// Filter to only include publishers with the developer feature flag
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var publisherIds = members.Select(m => m.Publisher.Id).ToList();
|
||||
var developerPublisherIds = await db.PublisherFeatures
|
||||
.Where(f => publisherIds.Contains(f.PublisherId))
|
||||
.Where(f => f.Flag == PublisherFeatureFlag.Develop)
|
||||
.Where(f => f.ExpiredAt == null || f.ExpiredAt > now)
|
||||
.Select(f => f.PublisherId)
|
||||
.ToListAsync();
|
||||
var developerQuery = db.Developers
|
||||
.Where(d => pubIds.Contains(d.PublisherId))
|
||||
.AsQueryable();
|
||||
|
||||
return members
|
||||
.Where(m => developerPublisherIds.Contains(m.Publisher.Id))
|
||||
.Select(m => m.Publisher)
|
||||
.ToList();
|
||||
var totalCount = await developerQuery.CountAsync();
|
||||
Response.Headers.Append("X-Total", totalCount.ToString());
|
||||
|
||||
var developers = await developerQuery.ToListAsync();
|
||||
|
||||
return Ok(developers);
|
||||
}
|
||||
|
||||
[HttpPost("{name}/enroll")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "developers.create")]
|
||||
public async Task<ActionResult<Publisher.Publisher>> EnrollDeveloperProgram(string name)
|
||||
public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var publisher = await db.Publishers
|
||||
.Where(p => p.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
PublisherInfo? pub;
|
||||
try
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||
pub = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
} catch (RpcException ex)
|
||||
{
|
||||
return NotFound(ex.Status.Detail);
|
||||
}
|
||||
|
||||
// Check if the user is an owner of the publisher
|
||||
var isOwner = await db.PublisherMembers
|
||||
.AnyAsync(m =>
|
||||
m.PublisherId == publisher.Id &&
|
||||
m.AccountId == accountId &&
|
||||
m.Role == PublisherMemberRole.Owner &&
|
||||
m.JoinedAt != null);
|
||||
|
||||
if (!isOwner) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
|
||||
|
||||
// Check if already has a developer feature
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var hasDeveloperFeature = await db.PublisherFeatures
|
||||
.AnyAsync(f =>
|
||||
f.PublisherId == publisher.Id &&
|
||||
f.Flag == PublisherFeatureFlag.Develop &&
|
||||
(f.ExpiredAt == null || f.ExpiredAt > now));
|
||||
|
||||
if (hasDeveloperFeature) return BadRequest("Publisher is already in the developer program");
|
||||
|
||||
// Add developer feature flag
|
||||
var feature = new PublisherFeature
|
||||
var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
|
||||
{
|
||||
PublisherId = publisher.Id,
|
||||
Flag = PublisherFeatureFlag.Develop,
|
||||
ExpiredAt = null
|
||||
PublisherId = pub.Id.ToString(),
|
||||
AccountId = currentUser.Id,
|
||||
Role = PublisherMemberRole.Owner
|
||||
});
|
||||
if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
|
||||
|
||||
var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
|
||||
if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
|
||||
|
||||
var developer = new Developer
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PublisherId = pub.Id
|
||||
};
|
||||
|
||||
db.PublisherFeatures.Add(feature);
|
||||
db.Developers.Add(developer);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
@@ -135,15 +110,15 @@ public class DeveloperController(
|
||||
Action = "developers.enroll",
|
||||
Meta =
|
||||
{
|
||||
{ "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(publisher.Id.ToString()) },
|
||||
{ "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(publisher.Name) }
|
||||
{ "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Id.ToString()) },
|
||||
{ "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Name) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(publisher);
|
||||
return Ok(developer);
|
||||
}
|
||||
|
||||
public class DeveloperStats
|
||||
|
49
DysonNetwork.Develop/Identity/DeveloperService.cs
Normal file
49
DysonNetwork.Develop/Identity/DeveloperService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class DeveloperService(AppDatabase db, PublisherService.PublisherServiceClient ps)
|
||||
{
|
||||
public async Task<Developer> LoadDeveloperPublisher(Developer developer)
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
|
||||
developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
return developer;
|
||||
}
|
||||
|
||||
public async Task<Developer?> GetDeveloperByName(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||
var pubId = Guid.Parse(pubResponse.Publisher.Id);
|
||||
|
||||
var developer = await db.Developers.FirstOrDefaultAsync(d => d.Id == pubId);
|
||||
return developer;
|
||||
}
|
||||
catch (RpcException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role)
|
||||
{
|
||||
try
|
||||
{
|
||||
var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
|
||||
{
|
||||
PublisherId = pubId.ToString(),
|
||||
AccountId = accountId.ToString(),
|
||||
Role = role
|
||||
});
|
||||
return permResponse.Valid;
|
||||
}
|
||||
catch (RpcException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
@@ -41,6 +42,9 @@ public static class ServiceCollectionExtensions
|
||||
options.SupportedUICultures = supportedCultures;
|
||||
});
|
||||
|
||||
services.AddScoped<DeveloperService>();
|
||||
services.AddScoped<CustomAppService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
@@ -14,20 +14,20 @@ public class VerificationMark
|
||||
[MaxLength(8192)] public string? Description { get; set; }
|
||||
[MaxLength(1024)] public string? VerifiedBy { get; set; }
|
||||
|
||||
public Shared.Proto.VerificationMark ToProtoValue()
|
||||
public Proto.VerificationMark ToProtoValue()
|
||||
{
|
||||
var proto = new Shared.Proto.VerificationMark
|
||||
var proto = new Proto.VerificationMark
|
||||
{
|
||||
Type = Type switch
|
||||
{
|
||||
VerificationMarkType.Official => Shared.Proto.VerificationMarkType.Official,
|
||||
VerificationMarkType.Individual => Shared.Proto.VerificationMarkType.Individual,
|
||||
VerificationMarkType.Organization => Shared.Proto.VerificationMarkType.Organization,
|
||||
VerificationMarkType.Government => Shared.Proto.VerificationMarkType.Government,
|
||||
VerificationMarkType.Creator => Shared.Proto.VerificationMarkType.Creator,
|
||||
VerificationMarkType.Developer => Shared.Proto.VerificationMarkType.Developer,
|
||||
VerificationMarkType.Parody => Shared.Proto.VerificationMarkType.Parody,
|
||||
_ => Shared.Proto.VerificationMarkType.Unspecified
|
||||
VerificationMarkType.Official => Proto.VerificationMarkType.Official,
|
||||
VerificationMarkType.Individual => Proto.VerificationMarkType.Individual,
|
||||
VerificationMarkType.Organization => Proto.VerificationMarkType.Organization,
|
||||
VerificationMarkType.Government => Proto.VerificationMarkType.Government,
|
||||
VerificationMarkType.Creator => Proto.VerificationMarkType.Creator,
|
||||
VerificationMarkType.Developer => Proto.VerificationMarkType.Developer,
|
||||
VerificationMarkType.Parody => Proto.VerificationMarkType.Parody,
|
||||
_ => Proto.VerificationMarkType.Unspecified
|
||||
},
|
||||
Title = Title ?? string.Empty,
|
||||
Description = Description ?? string.Empty,
|
||||
|
@@ -7,11 +7,12 @@ option csharp_namespace = "DysonNetwork.Shared.Proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "file.proto";
|
||||
import "account.proto";
|
||||
|
||||
enum PublisherType {
|
||||
PUBLISHER_TYPE_UNSPECIFIED = 0;
|
||||
INDIVIDUAL = 1;
|
||||
ORGANIZATIONAL = 2;
|
||||
PUB_INDIVIDUAL = 1;
|
||||
PUB_ORGANIZATIONAL = 2;
|
||||
}
|
||||
|
||||
enum PublisherMemberRole {
|
||||
@@ -48,33 +49,35 @@ message Publisher {
|
||||
google.protobuf.StringValue bio = 5;
|
||||
CloudFile picture = 8;
|
||||
CloudFile background = 9;
|
||||
optional bytes verification_mark = 10;
|
||||
optional VerificationMark verification_mark = 10;
|
||||
string account_id = 11;
|
||||
string realm_id = 12;
|
||||
optional string realm_id = 12;
|
||||
google.protobuf.Timestamp created_at = 13;
|
||||
google.protobuf.Timestamp updated_at = 14;
|
||||
}
|
||||
|
||||
message GetPublisherRequest {
|
||||
string name = 1;
|
||||
oneof query {
|
||||
string name = 1;
|
||||
string id = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message GetPublisherResponse {
|
||||
Publisher publisher = 1;
|
||||
}
|
||||
|
||||
message GetPublisherBatchRequest {
|
||||
repeated string ids = 1;
|
||||
}
|
||||
|
||||
message ListPublishersRequest {
|
||||
string account_id = 1; // filter by owner/member account
|
||||
string realm_id = 2; // filter by realm
|
||||
int32 page_size = 3;
|
||||
string page_token = 4;
|
||||
string order_by = 5;
|
||||
}
|
||||
|
||||
message ListPublishersResponse {
|
||||
repeated Publisher publishers = 1;
|
||||
string next_page_token = 2;
|
||||
int32 total_size = 3;
|
||||
}
|
||||
|
||||
message ListPublisherMembersRequest {
|
||||
@@ -99,10 +102,22 @@ message HasPublisherFeatureResponse {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message IsPublisherMemberRequest {
|
||||
string publisher_id = 1;
|
||||
string account_id = 2;
|
||||
PublisherMemberRole role = 3;
|
||||
}
|
||||
|
||||
message IsPublisherMemberResponse {
|
||||
bool valid = 1;
|
||||
}
|
||||
|
||||
service PublisherService {
|
||||
rpc GetPublisher(GetPublisherRequest) returns (GetPublisherResponse);
|
||||
rpc GetPublisherBatch(GetPublisherBatchRequest) returns (ListPublishersResponse);
|
||||
rpc ListPublishers(ListPublishersRequest) returns (ListPublishersResponse);
|
||||
rpc ListPublisherMembers(ListPublisherMembersRequest) returns (ListPublisherMembersResponse);
|
||||
rpc SetPublisherFeatureFlag(SetPublisherFeatureFlagRequest) returns (google.protobuf.StringValue); // returns optional message
|
||||
rpc HasPublisherFeature(HasPublisherFeatureRequest) returns (HasPublisherFeatureResponse);
|
||||
rpc IsPublisherMember(IsPublisherMemberRequest) returns (IsPublisherMemberResponse);
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ public enum PublisherType
|
||||
[Index(nameof(Name), IsUnique = true)]
|
||||
public class Publisher : ModelBase, IIdentifiedResource
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public PublisherType Type { get; set; }
|
||||
[MaxLength(256)] public string Name { get; set; } = string.Empty;
|
||||
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
@@ -57,8 +57,8 @@ public class Publisher : ModelBase, IIdentifiedResource
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Type = Type == PublisherType.Individual
|
||||
? Shared.Proto.PublisherType.Individual
|
||||
: Shared.Proto.PublisherType.Organizational,
|
||||
? Shared.Proto.PublisherType.PubIndividual
|
||||
: Shared.Proto.PublisherType.PubOrganizational,
|
||||
Name = Name,
|
||||
Nick = Nick,
|
||||
Bio = Bio,
|
||||
|
@@ -7,18 +7,50 @@ namespace DysonNetwork.Sphere.Publisher;
|
||||
public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
||||
: Shared.Proto.PublisherService.PublisherServiceBase
|
||||
{
|
||||
public override async Task<GetPublisherResponse> GetPublisher(GetPublisherRequest request,
|
||||
ServerCallContext context)
|
||||
public override async Task<GetPublisherResponse> GetPublisher(
|
||||
GetPublisherRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
var p = await service.GetPublisherByName(request.Name);
|
||||
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "publisher not found"));
|
||||
Publisher? p = null;
|
||||
switch (request.QueryCase)
|
||||
{
|
||||
case GetPublisherRequest.QueryOneofCase.Id:
|
||||
if (!string.IsNullOrWhiteSpace(request.Id) && Guid.TryParse(request.Id, out var id))
|
||||
p = await db.Publishers.FirstOrDefaultAsync(x => x.Id == id);
|
||||
break;
|
||||
case GetPublisherRequest.QueryOneofCase.Name:
|
||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
||||
p = await service.GetPublisherByName(request.Name);
|
||||
break;
|
||||
}
|
||||
|
||||
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "Publisher not found"));
|
||||
return new GetPublisherResponse { Publisher = p.ToProto(db) };
|
||||
}
|
||||
|
||||
public override async Task<ListPublishersResponse> ListPublishers(ListPublishersRequest request,
|
||||
ServerCallContext context)
|
||||
public override async Task<ListPublishersResponse> GetPublisherBatch(
|
||||
GetPublisherBatchRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
IQueryable<Publisher> query = db.Publishers.AsQueryable();
|
||||
var ids = request.Ids
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out _))
|
||||
.Select(Guid.Parse)
|
||||
.ToList();
|
||||
if (ids.Count == 0) return new ListPublishersResponse();
|
||||
var list = await db.Publishers.Where(p => ids.Contains(p.Id)).ToListAsync();
|
||||
var resp = new ListPublishersResponse();
|
||||
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
|
||||
return resp;
|
||||
}
|
||||
|
||||
public override async Task<ListPublishersResponse> ListPublishers(
|
||||
ListPublishersRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
var query = db.Publishers.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(request.AccountId) && Guid.TryParse(request.AccountId, out var aid))
|
||||
{
|
||||
var ids = await db.PublisherMembers.Where(m => m.AccountId == aid).Select(m => m.PublisherId).ToListAsync();
|
||||
@@ -26,14 +58,11 @@ public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
||||
{
|
||||
query = query.Where(p => p.RealmId == rid);
|
||||
}
|
||||
|
||||
var list = await query.Take(request.PageSize > 0 ? request.PageSize : 100).ToListAsync();
|
||||
var list = await query.ToListAsync();
|
||||
var resp = new ListPublishersResponse();
|
||||
resp.Publishers.AddRange(list.Select(p => p.ToProto(db)));
|
||||
resp.TotalSize = list.Count;
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -65,4 +94,22 @@ public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
||||
var enabled = await service.HasFeature(pid, request.Flag);
|
||||
return new HasPublisherFeatureResponse { Enabled = enabled };
|
||||
}
|
||||
|
||||
public override async Task<IsPublisherMemberResponse> IsPublisherMember(IsPublisherMemberRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
if (!Guid.TryParse(request.PublisherId, out var pid))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
|
||||
if (!Guid.TryParse(request.AccountId, out var aid))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid account_id"));
|
||||
var requiredRole = request.Role switch
|
||||
{
|
||||
Shared.Proto.PublisherMemberRole.Owner => PublisherMemberRole.Owner,
|
||||
Shared.Proto.PublisherMemberRole.Manager => PublisherMemberRole.Manager,
|
||||
Shared.Proto.PublisherMemberRole.Editor => PublisherMemberRole.Editor,
|
||||
_ => PublisherMemberRole.Viewer
|
||||
};
|
||||
var valid = await service.IsMemberWithRole(pid, aid, requiredRole);
|
||||
return new IsPublisherMemberResponse { Valid = valid };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user