Bot controller has keys endpoints

This commit is contained in:
2025-08-23 19:52:05 +08:00
parent d9620fd6a4
commit 953bf5d4de
5 changed files with 370 additions and 16 deletions

View File

@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
@@ -17,7 +19,8 @@ public class BotAccountController(
DeveloperService developerService,
DevProjectService projectService,
ILogger<BotAccountController> logger,
AccountClientHelper accounts
AccountClientHelper accounts,
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
)
: ControllerBase
{
@@ -85,8 +88,8 @@ public class BotAccountController(
return NotFound("Developer not found");
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to list bots");
PublisherMemberRole.Viewer))
return StatusCode(403, "You must be an viewer of the developer to list bots");
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null)
@@ -110,8 +113,8 @@ public class BotAccountController(
return NotFound("Developer not found");
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to view bot details");
PublisherMemberRole.Viewer))
return StatusCode(403, "You must be an viewer of the developer to view bot details");
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project is null)
@@ -291,4 +294,167 @@ public class BotAccountController(
return StatusCode(500, "An error occurred while deleting the bot account");
}
}
[HttpGet("{botId:guid}/keys")]
public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid botId
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
if (developer == null) return NotFound("Developer not found");
if (project == null) return NotFound("Project not found or you don't have access");
if (bot == null) return NotFound("Bot not found");
var keys = await accountsReceiver.ListApiKeyAsync(new ListApiKeyRequest
{
AutomatedId = bot.Id.ToString()
});
var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList();
return Ok(data);
}
[HttpGet("{botId:guid}/keys/{keyId:guid}")]
public async Task<ActionResult<ApiKeyReference>> GetBotKey(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid botId,
[FromRoute] Guid keyId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
if (developer == null) return NotFound("Developer not found");
if (project == null) return NotFound("Project not found or you don't have access");
if (bot == null) return NotFound("Bot not found");
try
{
var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
if (key == null) return NotFound("API key not found");
return Ok(ApiKeyReference.FromProtoValue(key));
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
{
return NotFound("API key not found");
}
}
public class CreateApiKeyRequest
{
[Required, MaxLength(1024)]
public string Label { get; set; } = null!;
}
[HttpPost("{botId:guid}/keys")]
public async Task<ActionResult<ApiKeyReference>> CreateBotKey(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid botId,
[FromBody] CreateApiKeyRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
if (developer == null) return NotFound("Developer not found");
if (project == null) return NotFound("Project not found or you don't have access");
if (bot == null) return NotFound("Bot not found");
try
{
var newKey = new ApiKey
{
AccountId = bot.Id.ToString(),
Label = request.Label
};
var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey);
return Ok(ApiKeyReference.FromProtoValue(createdKey));
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
{
return BadRequest(ex.Status.Detail);
}
}
[HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
public async Task<ActionResult<ApiKeyReference>> RotateBotKey(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid botId,
[FromRoute] Guid keyId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
if (developer == null) return NotFound("Developer not found");
if (project == null) return NotFound("Project not found or you don't have access");
if (bot == null) return NotFound("Bot not found");
try
{
var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
return Ok(ApiKeyReference.FromProtoValue(rotatedKey));
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
{
return NotFound("API key not found");
}
}
[HttpDelete("{botId:guid}/keys/{keyId:guid}")]
public async Task<IActionResult> DeleteBotKey(
[FromRoute] string pubName,
[FromRoute] Guid projectId,
[FromRoute] Guid botId,
[FromRoute] Guid keyId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
if (developer == null) return NotFound("Developer not found");
if (project == null) return NotFound("Project not found or you don't have access");
if (bot == null) return NotFound("Bot not found");
try
{
await accountsReceiver.DeleteApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
return NoContent();
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
{
return NotFound("API key not found");
}
}
private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess(
string pubName,
Guid projectId,
Guid botId,
Account currentUser,
PublisherMemberRole requiredRole)
{
var developer = await developerService.GetDeveloperByName(pubName);
if (developer == null) return (null, null, null);
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole))
return (null, null, null);
var project = await projectService.GetProjectAsync(projectId, developer.Id);
if (project == null) return (developer, null, null);
var bot = await botService.GetBotByIdAsync(botId);
if (bot == null || bot.ProjectId != projectId) return (developer, project, null);
return (developer, project, bot);
}
}

View File

@@ -1,8 +1,10 @@
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using NodaTime;
using Microsoft.EntityFrameworkCore;
using NodaTime.Serialization.Protobuf;
using ApiKey = DysonNetwork.Shared.Proto.ApiKey;
using AuthService = DysonNetwork.Pass.Auth.AuthService;
namespace DysonNetwork.Pass.Account;
@@ -10,7 +12,8 @@ public class BotAccountReceiverGrpc(
AppDatabase db,
AccountService accounts,
FileService.FileServiceClient files,
FileReferenceService.FileReferenceServiceClient fileRefs
FileReferenceService.FileReferenceServiceClient fileRefs,
AuthService authService
)
: BotAccountReceiverService.BotAccountReceiverServiceBase
{
@@ -107,10 +110,107 @@ public class BotAccountReceiverGrpc(
var automatedId = Guid.Parse(request.AutomatedId);
var account = await accounts.GetBotAccount(automatedId);
if (account is null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found"));
throw new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.NotFound, "Account not found"));
await accounts.DeleteAccount(account);
return new DeleteBotAccountResponse();
}
public override async Task<ApiKey> GetApiKey(GetApiKeyRequest request, ServerCallContext context)
{
var keyId = Guid.Parse(request.Id);
var key = await db.ApiKeys
.Include(k => k.Account)
.FirstOrDefaultAsync(k => k.Id == keyId);
if (key == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "API key not found"));
return key.ToProtoValue();
}
public override async Task<GetApiKeyBatchResponse> ListApiKey(ListApiKeyRequest request, ServerCallContext context)
{
var automatedId = Guid.Parse(request.AutomatedId);
var account = await accounts.GetBotAccount(automatedId);
if (account == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found"));
var keys = await db.ApiKeys
.Where(k => k.AccountId == account.Id)
.Select(k => k.ToProtoValue())
.ToListAsync();
var response = new GetApiKeyBatchResponse();
response.Data.AddRange(keys);
return response;
}
public override async Task<ApiKey> CreateApiKey(ApiKey request, ServerCallContext context)
{
var accountId = Guid.Parse(request.AccountId);
var account = await accounts.GetBotAccount(accountId);
if (account == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found"));
if (string.IsNullOrWhiteSpace(request.Label))
throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Label is required"));
var key = await authService.CreateApiKey(account.Id, request.Label, null);
key.Key = await authService.IssueApiKeyToken(key);
return key.ToProtoValue();
}
public override async Task<ApiKey> UpdateApiKey(ApiKey request, ServerCallContext context)
{
var keyId = Guid.Parse(request.Id);
var accountId = Guid.Parse(request.AccountId);
var key = await db.ApiKeys
.FirstOrDefaultAsync(k => k.Id == keyId && k.AccountId == accountId);
if (key == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "API key not found"));
// Only update the label if provided
if (string.IsNullOrWhiteSpace(request.Label)) return key.ToProtoValue();
key.Label = request.Label;
db.ApiKeys.Update(key);
await db.SaveChangesAsync();
return key.ToProtoValue();
}
public override async Task<ApiKey> RotateApiKey(GetApiKeyRequest request, ServerCallContext context)
{
var keyId = Guid.Parse(request.Id);
var key = await db.ApiKeys
.Include(k => k.Account)
.FirstOrDefaultAsync(k => k.Id == keyId);
if (key == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "API key not found"));
key = await authService.RotateApiKeyToken(key);
key.Key = await authService.IssueApiKeyToken(key);
return key.ToProtoValue();
}
public override async Task<DeleteApiKeyResponse> DeleteApiKey(GetApiKeyRequest request, ServerCallContext context)
{
var keyId = Guid.Parse(request.Id);
var key = await db.ApiKeys
.Include(k => k.Account)
.FirstOrDefaultAsync(k => k.Id == keyId);
if (key == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "API key not found"));
await authService.RevokeApiKeyToken(key);
return new DeleteApiKeyResponse { Success = true };
}
}

View File

@@ -9,11 +9,37 @@ public class ApiKey : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Label { get; set; } = null!;
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Guid SessionId { get; set; }
public AuthSession Session { get; set; } = null!;
[NotMapped] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Key { get; set; }
[NotMapped]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Key { get; set; }
public DysonNetwork.Shared.Proto.ApiKey ToProtoValue()
{
return new DysonNetwork.Shared.Proto.ApiKey
{
Id = Id.ToString(),
Label = Label,
AccountId = AccountId.ToString(),
SessionId = SessionId.ToString(),
Key = Key
};
}
public static ApiKey FromProtoValue(DysonNetwork.Shared.Proto.ApiKey proto)
{
return new ApiKey
{
Id = Guid.Parse(proto.Id),
AccountId = Guid.Parse(proto.AccountId),
SessionId = Guid.Parse(proto.SessionId),
Label = proto.Label,
Key = proto.Key
};
}
}

View File

@@ -308,4 +308,39 @@ public static class Leveling
512000, // Level 13
1024000
];
}
public class ApiKeyReference : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Label { get; set; } = null!;
public Guid AccountId { get; set; }
public Guid SessionId { get; set; }
public string? Key { get; set; }
public ApiKey ToProtoValue()
{
return new ApiKey
{
Id = Id.ToString(),
Label = Label,
AccountId = AccountId.ToString(),
SessionId = SessionId.ToString(),
Key = Key
};
}
public static ApiKeyReference FromProtoValue(ApiKey proto)
{
return new ApiKeyReference
{
Id = Guid.Parse(proto.Id),
AccountId = Guid.Parse(proto.AccountId),
SessionId = Guid.Parse(proto.SessionId),
Label = proto.Label,
Key = proto.Key
};
}
}

View File

@@ -5,6 +5,7 @@ package proto;
option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "account.proto";
import "file.proto";
@@ -136,15 +137,41 @@ message DeleteBotAccountResponse {
bool success = 1; // Whether the deletion was successful
}
message ApiKey {
string id = 1;
string label = 2;
string account_id = 3;
string session_id = 4;
google.protobuf.StringValue key = 5;
}
message GetApiKeyRequest {
string id = 1;
}
message ListApiKeyRequest {
string automated_id = 1;
}
message GetApiKeyBatchResponse {
repeated ApiKey data = 1;
}
message DeleteApiKeyResponse {
bool success = 1;
}
// This service should be implemented by the Pass service to handle the creation, update, and deletion of bot accounts
service BotAccountReceiverService {
// Create a new bot account
rpc CreateBotAccount(CreateBotAccountRequest) returns (CreateBotAccountResponse);
// Update an existing bot account
rpc UpdateBotAccount(UpdateBotAccountRequest) returns (UpdateBotAccountResponse);
// Delete a bot account
rpc DeleteBotAccount(DeleteBotAccountRequest) returns (DeleteBotAccountResponse);
rpc GetApiKey(GetApiKeyRequest) returns (ApiKey);
rpc ListApiKey(ListApiKeyRequest) returns (GetApiKeyBatchResponse);
rpc CreateApiKey(ApiKey) returns (ApiKey);
rpc UpdateApiKey(ApiKey) returns (ApiKey);
rpc RotateApiKey(GetApiKeyRequest) returns (ApiKey);
rpc DeleteApiKey(GetApiKeyRequest) returns (DeleteApiKeyResponse);
}