From ee8e9df12e816d3b11727b92808a46f833ce499e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 7 Aug 2025 20:30:34 +0800 Subject: [PATCH] :sparkles: Complete the develop service --- DysonNetwork.Develop/AppDatabase.cs | 6 + DysonNetwork.Develop/Identity/CustomApp.cs | 4 +- .../Identity/CustomAppController.cs | 46 +++---- .../Identity/CustomAppService.cs | 13 +- DysonNetwork.Develop/Identity/Developer.cs | 75 ++++++++++ .../Identity/DeveloperController.cs | 129 +++++++----------- .../Identity/DeveloperService.cs | 49 +++++++ .../Startup/ServiceCollectionExtensions.cs | 4 + DysonNetwork.Shared/Data/VerificationMark.cs | 20 +-- DysonNetwork.Shared/Proto/publisher.proto | 35 +++-- DysonNetwork.Sphere/Publisher/Publisher.cs | 6 +- .../Publisher/PublisherServiceGrpc.cs | 69 ++++++++-- 12 files changed, 313 insertions(+), 143 deletions(-) create mode 100644 DysonNetwork.Develop/Identity/Developer.cs create mode 100644 DysonNetwork.Develop/Identity/DeveloperService.cs diff --git a/DysonNetwork.Develop/AppDatabase.cs b/DysonNetwork.Develop/AppDatabase.cs index 205ffbc..c1b9cc2 100644 --- a/DysonNetwork.Develop/AppDatabase.cs +++ b/DysonNetwork.Develop/AppDatabase.cs @@ -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 Developers { get; set; } + + public DbSet CustomApps { get; set; } + public DbSet CustomAppSecrets { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql( diff --git a/DysonNetwork.Develop/Identity/CustomApp.cs b/DysonNetwork.Develop/Identity/CustomApp.cs index 6b03fa1..a5a84b3 100644 --- a/DysonNetwork.Develop/Identity/CustomApp.cs +++ b/DysonNetwork.Develop/Identity/CustomApp.cs @@ -31,8 +31,8 @@ public class CustomApp : ModelBase, IIdentifiedResource [JsonIgnore] public ICollection Secrets { get; set; } = new List(); - 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; } diff --git a/DysonNetwork.Develop/Identity/CustomAppController.cs b/DysonNetwork.Develop/Identity/CustomAppController.cs index b6ecd38..3fd7600 100644 --- a/DysonNetwork.Develop/Identity/CustomAppController.cs +++ b/DysonNetwork.Develop/Identity/CustomAppController.cs @@ -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 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 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(); diff --git a/DysonNetwork.Develop/Identity/CustomAppService.cs b/DysonNetwork.Develop/Identity/CustomAppService.cs index b3932d3..e065998 100644 --- a/DysonNetwork.Develop/Identity/CustomAppService.cs +++ b/DysonNetwork.Develop/Identity/CustomAppService.cs @@ -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 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 GetAppAsync(Guid id, Guid? publisherId = null) + public async Task 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> 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 UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request) diff --git a/DysonNetwork.Develop/Identity/Developer.cs b/DysonNetwork.Develop/Identity/Developer.cs new file mode 100644 index 0000000..0fcb98b --- /dev/null +++ b/DysonNetwork.Develop/Identity/Developer.cs @@ -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; + } +} \ No newline at end of file diff --git a/DysonNetwork.Develop/Identity/DeveloperController.cs b/DysonNetwork.Develop/Identity/DeveloperController.cs index 2a0da50..f7ddc74 100644 --- a/DysonNetwork.Develop/Identity/DeveloperController.cs +++ b/DysonNetwork.Develop/Identity/DeveloperController.cs @@ -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> GetDeveloper(string name) + public async Task> 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> 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>> ListJoinedDevelopers() + public async Task>> ListJoinedDevelopers() { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); + + var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id }); + var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList(); - var members = await db.PublisherMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.JoinedAt != null) - .Include(e => e.Publisher) - .ToListAsync(); + var developerQuery = db.Developers + .Where(d => pubIds.Contains(d.PublisherId)) + .AsQueryable(); + + var totalCount = await developerQuery.CountAsync(); + Response.Headers.Append("X-Total", totalCount.ToString()); + + var developers = await developerQuery.ToListAsync(); - // 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(); - - return members - .Where(m => developerPublisherIds.Contains(m.Publisher.Id)) - .Select(m => m.Publisher) - .ToList(); + return Ok(developers); } [HttpPost("{name}/enroll")] [Authorize] [RequiredPermission("global", "developers.create")] - public async Task> EnrollDeveloperProgram(string name) + public async Task> 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 diff --git a/DysonNetwork.Develop/Identity/DeveloperService.cs b/DysonNetwork.Develop/Identity/DeveloperService.cs new file mode 100644 index 0000000..8681ad2 --- /dev/null +++ b/DysonNetwork.Develop/Identity/DeveloperService.cs @@ -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 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 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 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; + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs index f924c79..17defbb 100644 --- a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs @@ -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(); + services.AddScoped(); + return services; } diff --git a/DysonNetwork.Shared/Data/VerificationMark.cs b/DysonNetwork.Shared/Data/VerificationMark.cs index 45eb071..912abd1 100644 --- a/DysonNetwork.Shared/Data/VerificationMark.cs +++ b/DysonNetwork.Shared/Data/VerificationMark.cs @@ -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, diff --git a/DysonNetwork.Shared/Proto/publisher.proto b/DysonNetwork.Shared/Proto/publisher.proto index 5b75656..58b395a 100644 --- a/DysonNetwork.Shared/Proto/publisher.proto +++ b/DysonNetwork.Shared/Proto/publisher.proto @@ -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); } diff --git a/DysonNetwork.Sphere/Publisher/Publisher.cs b/DysonNetwork.Sphere/Publisher/Publisher.cs index ec6a7a3..9f826d1 100644 --- a/DysonNetwork.Sphere/Publisher/Publisher.cs +++ b/DysonNetwork.Sphere/Publisher/Publisher.cs @@ -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, diff --git a/DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs b/DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs index 262a498..8c748f3 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherServiceGrpc.cs @@ -7,18 +7,50 @@ namespace DysonNetwork.Sphere.Publisher; public class PublisherServiceGrpc(PublisherService service, AppDatabase db) : Shared.Proto.PublisherService.PublisherServiceBase { - public override async Task GetPublisher(GetPublisherRequest request, - ServerCallContext context) + public override async Task 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 ListPublishers(ListPublishersRequest request, - ServerCallContext context) + public override async Task GetPublisherBatch( + GetPublisherBatchRequest request, + ServerCallContext context + ) { - IQueryable 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 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 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 }; + } } \ No newline at end of file