461 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			461 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.ComponentModel.DataAnnotations;
 | |
| using DysonNetwork.Develop.Project;
 | |
| using DysonNetwork.Shared.Models;
 | |
| using DysonNetwork.Shared.Proto;
 | |
| using DysonNetwork.Shared.Registry;
 | |
| using Grpc.Core;
 | |
| using Microsoft.AspNetCore.Authorization;
 | |
| using Microsoft.AspNetCore.Mvc;
 | |
| using NodaTime;
 | |
| using NodaTime.Serialization.Protobuf;
 | |
| 
 | |
| namespace DysonNetwork.Develop.Identity;
 | |
| 
 | |
| [ApiController]
 | |
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")]
 | |
| [Authorize]
 | |
| public class BotAccountController(
 | |
|     BotAccountService botService,
 | |
|     DeveloperService ds,
 | |
|     DevProjectService projectService,
 | |
|     ILogger<BotAccountController> logger,
 | |
|     RemoteAccountService remoteAccounts,
 | |
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
 | |
| )
 | |
|     : ControllerBase
 | |
| {
 | |
|     public class CommonBotRequest
 | |
|     {
 | |
|         [MaxLength(256)] public string? FirstName { get; set; }
 | |
|         [MaxLength(256)] public string? MiddleName { get; set; }
 | |
|         [MaxLength(256)] public string? LastName { get; set; }
 | |
|         [MaxLength(1024)] public string? Gender { get; set; }
 | |
|         [MaxLength(1024)] public string? Pronouns { get; set; }
 | |
|         [MaxLength(1024)] public string? TimeZone { get; set; }
 | |
|         [MaxLength(1024)] public string? Location { get; set; }
 | |
|         [MaxLength(4096)] public string? Bio { get; set; }
 | |
|         public Instant? Birthday { get; set; }
 | |
| 
 | |
|         [MaxLength(32)] public string? PictureId { get; set; }
 | |
|         [MaxLength(32)] public string? BackgroundId { get; set; }
 | |
|     }
 | |
| 
 | |
|     public class BotCreateRequest : CommonBotRequest
 | |
|     {
 | |
|         [Required]
 | |
|         [MinLength(2)]
 | |
|         [MaxLength(256)]
 | |
|         [RegularExpression(@"^[A-Za-z0-9_-]+$",
 | |
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
 | |
|         ]
 | |
|         public string Name { get; set; } = string.Empty;
 | |
| 
 | |
|         [Required][MaxLength(256)] public string Nick { get; set; } = string.Empty;
 | |
| 
 | |
|         [Required][MaxLength(1024)] public string Slug { get; set; } = string.Empty;
 | |
| 
 | |
|         [MaxLength(128)] public string Language { get; set; } = "en-us";
 | |
|     }
 | |
| 
 | |
|     public class UpdateBotRequest : CommonBotRequest
 | |
|     {
 | |
|         [MinLength(2)]
 | |
|         [MaxLength(256)]
 | |
|         [RegularExpression(@"^[A-Za-z0-9_-]+$",
 | |
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
 | |
|         ]
 | |
|         public string? Name { get; set; } = string.Empty;
 | |
| 
 | |
|         [MaxLength(256)] public string? Nick { get; set; } = string.Empty;
 | |
| 
 | |
|         [Required][MaxLength(1024)] public string? Slug { get; set; } = string.Empty;
 | |
| 
 | |
|         [MaxLength(128)] public string? Language { get; set; }
 | |
| 
 | |
|         public bool? IsActive { get; set; }
 | |
|     }
 | |
| 
 | |
|     [HttpGet]
 | |
|     public async Task<IActionResult> ListBots(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | |
|                 Shared.Proto.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)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var bots = await botService.GetBotsByProjectAsync(projectId);
 | |
|         return Ok(await botService.LoadBotsAccountAsync(bots));
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{botId:guid}")]
 | |
|     public async Task<IActionResult> GetBot(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid botId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | |
|                 Shared.Proto.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)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var bot = await botService.GetBotByIdAsync(botId);
 | |
|         if (bot is null || bot.ProjectId != projectId)
 | |
|             return NotFound("Bot not found");
 | |
| 
 | |
|         return Ok(await botService.LoadBotAccountAsync(bot));
 | |
|     }
 | |
| 
 | |
|     [HttpPost]
 | |
|     public async Task<IActionResult> CreateBot(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromBody] BotCreateRequest createRequest
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | |
|                 Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to create a bot");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var now = SystemClock.Instance.GetCurrentInstant();
 | |
|         var accountId = Guid.NewGuid();
 | |
|         var account = new Account()
 | |
|         {
 | |
|             Id = accountId.ToString(),
 | |
|             Name = createRequest.Name,
 | |
|             Nick = createRequest.Nick,
 | |
|             Language = createRequest.Language,
 | |
|             Profile = new AccountProfile()
 | |
|             {
 | |
|                 Id = Guid.NewGuid().ToString(),
 | |
|                 Bio = createRequest.Bio,
 | |
|                 Gender = createRequest.Gender,
 | |
|                 FirstName = createRequest.FirstName,
 | |
|                 MiddleName = createRequest.MiddleName,
 | |
|                 LastName = createRequest.LastName,
 | |
|                 TimeZone = createRequest.TimeZone,
 | |
|                 Pronouns = createRequest.Pronouns,
 | |
|                 Location = createRequest.Location,
 | |
|                 Birthday = createRequest.Birthday?.ToTimestamp(),
 | |
|                 AccountId = accountId.ToString(),
 | |
|                 CreatedAt = now.ToTimestamp(),
 | |
|                 UpdatedAt = now.ToTimestamp()
 | |
|             },
 | |
|             CreatedAt = now.ToTimestamp(),
 | |
|             UpdatedAt = now.ToTimestamp()
 | |
|         };
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var bot = await botService.CreateBotAsync(
 | |
|                 project,
 | |
|                 createRequest.Slug,
 | |
|                 account,
 | |
|                 createRequest.PictureId,
 | |
|                 createRequest.BackgroundId
 | |
|             );
 | |
|             return Ok(bot);
 | |
|         }
 | |
|         catch (Exception ex)
 | |
|         {
 | |
|             logger.LogError(ex, "Error creating bot account");
 | |
|             return StatusCode(500, "An error occurred while creating the bot account");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpPatch("{botId:guid}")]
 | |
|     public async Task<IActionResult> UpdateBot(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid botId,
 | |
|         [FromBody] UpdateBotRequest request
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | |
|                 Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to update a bot");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var bot = await botService.GetBotByIdAsync(botId);
 | |
|         if (bot is null || bot.ProjectId != projectId)
 | |
|             return NotFound("Bot not found");
 | |
| 
 | |
|         var botAccount = await remoteAccounts.GetBotAccount(bot.Id);
 | |
| 
 | |
|         if (request.Name is not null) botAccount.Name = request.Name;
 | |
|         if (request.Nick is not null) botAccount.Nick = request.Nick;
 | |
|         if (request.Language is not null) botAccount.Language = request.Language;
 | |
|         if (request.Bio is not null) botAccount.Profile.Bio = request.Bio;
 | |
|         if (request.Gender is not null) botAccount.Profile.Gender = request.Gender;
 | |
|         if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName;
 | |
|         if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName;
 | |
|         if (request.LastName is not null) botAccount.Profile.LastName = request.LastName;
 | |
|         if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone;
 | |
|         if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns;
 | |
|         if (request.Location is not null) botAccount.Profile.Location = request.Location;
 | |
|         if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp();
 | |
| 
 | |
|         if (request.Slug is not null) bot.Slug = request.Slug;
 | |
|         if (request.IsActive is not null) bot.IsActive = request.IsActive.Value;
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var updatedBot = await botService.UpdateBotAsync(
 | |
|                 bot,
 | |
|                 botAccount,
 | |
|                 request.PictureId,
 | |
|                 request.BackgroundId
 | |
|             );
 | |
| 
 | |
|             return Ok(updatedBot);
 | |
|         }
 | |
|         catch (Exception ex)
 | |
|         {
 | |
|             logger.LogError(ex, "Error updating bot account {BotId}", botId);
 | |
|             return StatusCode(500, "An error occurred while updating the bot account");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpDelete("{botId:guid}")]
 | |
|     public async Task<IActionResult> DeleteBot(
 | |
|         [FromRoute] string pubName,
 | |
|         [FromRoute] Guid projectId,
 | |
|         [FromRoute] Guid botId)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | |
|             return Unauthorized();
 | |
| 
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer is null)
 | |
|             return NotFound("Developer not found");
 | |
| 
 | |
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | |
|                 Shared.Proto.PublisherMemberRole.Editor))
 | |
|             return StatusCode(403, "You must be an editor of the developer to delete a bot");
 | |
| 
 | |
|         var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | |
|         if (project is null)
 | |
|             return NotFound("Project not found or you don't have access");
 | |
| 
 | |
|         var bot = await botService.GetBotByIdAsync(botId);
 | |
|         if (bot is null || bot.ProjectId != projectId)
 | |
|             return NotFound("Bot not found");
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             await botService.DeleteBotAsync(bot);
 | |
|             return NoContent();
 | |
|         }
 | |
|         catch (Exception ex)
 | |
|         {
 | |
|             logger.LogError(ex, "Error deleting bot {BotId}", botId);
 | |
|             return StatusCode(500, "An error occurred while deleting the bot account");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{botId:guid}/keys")]
 | |
|     public async Task<ActionResult<List<SnApiKey>>> 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, Shared.Proto.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(SnApiKey.FromProtoValue).ToList();
 | |
| 
 | |
|         return Ok(data);
 | |
|     }
 | |
| 
 | |
|     [HttpGet("{botId:guid}/keys/{keyId:guid}")]
 | |
|     public async Task<ActionResult<SnApiKey>> 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, Shared.Proto.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(SnApiKey.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<SnApiKey>> 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, Shared.Proto.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(SnApiKey.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<SnApiKey>> 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, Shared.Proto.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(SnApiKey.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, Shared.Proto.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<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess(
 | |
|         string pubName,
 | |
|         Guid projectId,
 | |
|         Guid botId,
 | |
|         Account currentUser,
 | |
|         Shared.Proto.PublisherMemberRole requiredRole)
 | |
|     {
 | |
|         var developer = await ds.GetDeveloperByName(pubName);
 | |
|         if (developer == null) return (null, null, null);
 | |
| 
 | |
|         if (!await ds.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);
 | |
|     }
 | |
| }
 |