Develop service

This commit is contained in:
2025-08-08 00:47:26 +08:00
parent a6dfe8712c
commit 77ccc9aeb5
43 changed files with 4842 additions and 584 deletions

View File

@@ -2,8 +2,17 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using NodaTime.Serialization.Protobuf;
using NodaTime;
using DysonNetwork.Shared.Proto;
using Google.Protobuf.WellKnownTypes;
using NodaTime.Serialization.Protobuf;
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
namespace DysonNetwork.Develop.Identity;
public enum CustomAppStatus
@@ -25,7 +34,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public DysonNetwork.Shared.Data.VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
@@ -35,6 +44,66 @@ public class CustomApp : ModelBase, IIdentifiedResource
public Developer Developer { get; set; } = null!;
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
public Shared.Proto.CustomApp ToProto()
{
return new Shared.Proto.CustomApp
{
Id = Id.ToString(),
Slug = Slug,
Name = Name,
Description = Description ?? string.Empty,
Status = Status switch
{
CustomAppStatus.Developing => Shared.Proto.CustomAppStatus.Developing,
CustomAppStatus.Staging => Shared.Proto.CustomAppStatus.Staging,
CustomAppStatus.Production => Shared.Proto.CustomAppStatus.Production,
CustomAppStatus.Suspended => Shared.Proto.CustomAppStatus.Suspended,
_ => Shared.Proto.CustomAppStatus.Unspecified
},
Picture = Picture is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Picture)),
Background = Background is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Background)),
Verification = Verification is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Verification)),
Links = Links is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Links)),
OauthConfig = OauthConfig is null ? null : new DysonNetwork.Shared.Proto.CustomAppOauthConfig
{
ClientUri = OauthConfig.ClientUri ?? string.Empty,
RedirectUris = { OauthConfig.RedirectUris ?? Array.Empty<string>() },
PostLogoutRedirectUris = { OauthConfig.PostLogoutRedirectUris ?? Array.Empty<string>() },
AllowedScopes = { OauthConfig.AllowedScopes ?? Array.Empty<string>() },
AllowedGrantTypes = { OauthConfig.AllowedGrantTypes ?? Array.Empty<string>() },
RequirePkce = OauthConfig.RequirePkce,
AllowOfflineAccess = OauthConfig.AllowOfflineAccess
},
DeveloperId = DeveloperId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
}
public CustomApp FromProtoValue(Shared.Proto.CustomApp p)
{
Id = Guid.Parse(p.Id);
Slug = p.Slug;
Name = p.Name;
Description = string.IsNullOrEmpty(p.Description) ? null : p.Description;
Status = p.Status switch
{
Shared.Proto.CustomAppStatus.Developing => CustomAppStatus.Developing,
Shared.Proto.CustomAppStatus.Staging => CustomAppStatus.Staging,
Shared.Proto.CustomAppStatus.Production => CustomAppStatus.Production,
Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended,
_ => CustomAppStatus.Developing
};
DeveloperId = string.IsNullOrEmpty(p.DeveloperId) ? Guid.Empty : Guid.Parse(p.DeveloperId);
CreatedAt = p.CreatedAt.ToInstant();
UpdatedAt = p.UpdatedAt.ToInstant();
if (p.Picture.Length > 0) Picture = System.Text.Json.JsonSerializer.Deserialize<CloudFileReferenceObject>(p.Picture.ToStringUtf8());
if (p.Background.Length > 0) Background = System.Text.Json.JsonSerializer.Deserialize<CloudFileReferenceObject>(p.Background.ToStringUtf8());
if (p.Verification.Length > 0) Verification = System.Text.Json.JsonSerializer.Deserialize<DysonNetwork.Shared.Data.VerificationMark>(p.Verification.ToStringUtf8());
if (p.Links.Length > 0) Links = System.Text.Json.JsonSerializer.Deserialize<CustomAppLinks>(p.Links.ToStringUtf8());
return this;
}
}
public class CustomAppLinks
@@ -65,4 +134,31 @@ public class CustomAppSecret : ModelBase
public Guid AppId { get; set; }
public CustomApp App { get; set; } = null!;
public static CustomAppSecret FromProtoValue(DysonNetwork.Shared.Proto.CustomAppSecret p)
{
return new CustomAppSecret
{
Id = Guid.Parse(p.Id),
Secret = p.Secret,
Description = p.Description,
ExpiredAt = p.ExpiredAt?.ToInstant(),
IsOidc = p.IsOidc,
AppId = Guid.Parse(p.AppId),
};
}
public DysonNetwork.Shared.Proto.CustomAppSecret ToProto()
{
return new DysonNetwork.Shared.Proto.CustomAppSecret
{
Id = Id.ToString(),
Secret = Secret,
Description = Description,
ExpiredAt = ExpiredAt?.ToTimestamp(),
IsOidc = IsOidc,
AppId = Id.ToString(),
};
}
}

View File

@@ -0,0 +1,68 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Develop.Identity;
public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppService.CustomAppServiceBase
{
public override async Task<GetCustomAppResponse> GetCustomApp(GetCustomAppRequest request, ServerCallContext context)
{
var q = db.CustomApps.AsQueryable();
switch (request.QueryCase)
{
case GetCustomAppRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id):
{
if (!Guid.TryParse(request.Id, out var id))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid id"));
var appById = await q.FirstOrDefaultAsync(a => a.Id == id);
if (appById is null)
throw new RpcException(new Status(StatusCode.NotFound, "app not found"));
return new GetCustomAppResponse { App = appById.ToProto() };
}
case GetCustomAppRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug):
{
var appBySlug = await q.FirstOrDefaultAsync(a => a.Slug == request.Slug);
if (appBySlug is null)
throw new RpcException(new Status(StatusCode.NotFound, "app not found"));
return new GetCustomAppResponse { App = appBySlug.ToProto() };
}
default:
throw new RpcException(new Status(StatusCode.InvalidArgument, "id or slug required"));
}
}
public override async Task<CheckCustomAppSecretResponse> CheckCustomAppSecret(CheckCustomAppSecretRequest request, ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Secret))
throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
IQueryable<CustomAppSecret> q = db.CustomAppSecrets;
switch (request.SecretIdentifierCase)
{
case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId:
{
if (!Guid.TryParse(request.SecretId, out var sid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid secret_id"));
q = q.Where(s => s.Id == sid);
break;
}
case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.AppId:
{
if (!Guid.TryParse(request.AppId, out var aid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid app_id"));
q = q.Where(s => s.AppId == aid);
break;
}
default:
throw new RpcException(new Status(StatusCode.InvalidArgument, "secret_id or app_id required"));
}
if (request.HasIsOidc)
q = q.Where(s => s.IsOidc == request.IsOidc);
var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
var exists = await q.AnyAsync(s => s.Secret == request.Secret && (s.ExpiredAt == null || s.ExpiredAt > now));
return new CheckCustomAppSecretResponse { Valid = exists };
}
}