Complete the develop service

This commit is contained in:
2025-08-07 20:30:34 +08:00
parent 00cdd1bc5d
commit ee8e9df12e
12 changed files with 313 additions and 143 deletions

View File

@@ -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(

View File

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

View File

@@ -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();

View File

@@ -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)

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

View File

@@ -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 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<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

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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