Compare commits
2 Commits
c056938b6e
...
7f7b47fb1c
Author | SHA1 | Date | |
---|---|---|---|
|
7f7b47fb1c | ||
|
bf181b88ec |
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using DysonNetwork.Develop.Identity;
|
using DysonNetwork.Develop.Identity;
|
||||||
using DysonNetwork.Develop.Project;
|
using DysonNetwork.Develop.Project;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -16,6 +17,7 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public DbSet<CustomApp> CustomApps { get; set; } = null!;
|
public DbSet<CustomApp> CustomApps { get; set; } = null!;
|
||||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||||
|
public DbSet<BotAccount> BotAccounts { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
46
DysonNetwork.Develop/Identity/BotAccount.cs
Normal file
46
DysonNetwork.Develop/Identity/BotAccount.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Develop.Project;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.Protobuf;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Develop.Identity;
|
||||||
|
|
||||||
|
public class BotAccount : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public Guid ProjectId { get; set; }
|
||||||
|
public DevProject Project { get; set; } = null!;
|
||||||
|
|
||||||
|
public Shared.Proto.BotAccount ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new Shared.Proto.BotAccount
|
||||||
|
{
|
||||||
|
Slug = Slug,
|
||||||
|
IsActive = IsActive,
|
||||||
|
AutomatedId = Id.ToString(),
|
||||||
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
|
};
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BotAccount FromProto(Shared.Proto.BotAccount proto)
|
||||||
|
{
|
||||||
|
var botAccount = new BotAccount
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.AutomatedId),
|
||||||
|
Slug = proto.Slug,
|
||||||
|
IsActive = proto.IsActive,
|
||||||
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||||
|
};
|
||||||
|
|
||||||
|
return botAccount;
|
||||||
|
}
|
||||||
|
}
|
197
DysonNetwork.Develop/Identity/BotAccountController.cs
Normal file
197
DysonNetwork.Develop/Identity/BotAccountController.cs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Develop.Project;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Develop.Identity;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")]
|
||||||
|
[Authorize]
|
||||||
|
public class BotAccountController(
|
||||||
|
BotAccountService botService,
|
||||||
|
DeveloperService developerService,
|
||||||
|
DevProjectService projectService,
|
||||||
|
ILogger<BotAccountController> logger
|
||||||
|
)
|
||||||
|
: ControllerBase
|
||||||
|
{
|
||||||
|
public record BotRequest(
|
||||||
|
[Required] [MaxLength(1024)] string? Slug
|
||||||
|
);
|
||||||
|
|
||||||
|
public record UpdateBotRequest(
|
||||||
|
[MaxLength(1024)] string? Slug,
|
||||||
|
bool? IsActive
|
||||||
|
) : BotRequest(Slug);
|
||||||
|
|
||||||
|
[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 developerService.GetDeveloperByName(pubName);
|
||||||
|
if (developer is null)
|
||||||
|
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");
|
||||||
|
|
||||||
|
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(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 developerService.GetDeveloperByName(pubName);
|
||||||
|
if (developer is null)
|
||||||
|
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");
|
||||||
|
|
||||||
|
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(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateBot(
|
||||||
|
[FromRoute] string pubName,
|
||||||
|
[FromRoute] Guid projectId,
|
||||||
|
[FromBody] BotRequest request
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Slug))
|
||||||
|
return BadRequest("Name is required");
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var developer = await developerService.GetDeveloperByName(pubName);
|
||||||
|
if (developer is null)
|
||||||
|
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 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");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bot = await botService.CreateBotAsync(project, request.Slug);
|
||||||
|
return Ok(bot);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error creating bot account");
|
||||||
|
return StatusCode(500, "An error occurred while creating the bot account");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{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 developerService.GetDeveloperByName(pubName);
|
||||||
|
if (developer is null)
|
||||||
|
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 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");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updatedBot = await botService.UpdateBotAsync(
|
||||||
|
bot,
|
||||||
|
request.Slug,
|
||||||
|
request.IsActive
|
||||||
|
);
|
||||||
|
|
||||||
|
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 developerService.GetDeveloperByName(pubName);
|
||||||
|
if (developer is null)
|
||||||
|
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 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
168
DysonNetwork.Develop/Identity/BotAccountService.cs
Normal file
168
DysonNetwork.Develop/Identity/BotAccountService.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
using DysonNetwork.Develop.Project;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.Protobuf;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Develop.Identity;
|
||||||
|
|
||||||
|
public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver)
|
||||||
|
{
|
||||||
|
public async Task<BotAccount?> GetBotByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return await db.BotAccounts
|
||||||
|
.Include(b => b.Project)
|
||||||
|
.FirstOrDefaultAsync(b => b.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId)
|
||||||
|
{
|
||||||
|
return await db.BotAccounts
|
||||||
|
.Where(b => b.ProjectId == projectId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BotAccount> CreateBotAsync(DevProject project, string slug)
|
||||||
|
{
|
||||||
|
// First, check if a bot with this slug already exists in this project
|
||||||
|
var existingBot = await db.BotAccounts
|
||||||
|
.FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug);
|
||||||
|
|
||||||
|
if (existingBot != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A bot with this slug already exists in this project.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First create the bot account in the Pass service
|
||||||
|
var createRequest = new CreateBotAccountRequest
|
||||||
|
{
|
||||||
|
AutomatedId = Guid.NewGuid().ToString(),
|
||||||
|
Account = new Account
|
||||||
|
{
|
||||||
|
Name = slug,
|
||||||
|
Nick = $"Bot {slug}",
|
||||||
|
Language = "en",
|
||||||
|
Profile = new AccountProfile
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
CreatedAt = now.ToTimestamp(),
|
||||||
|
UpdatedAt = now.ToTimestamp()
|
||||||
|
},
|
||||||
|
CreatedAt = now.ToTimestamp(),
|
||||||
|
UpdatedAt = now.ToTimestamp()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest);
|
||||||
|
var botAccount = createResponse.Bot;
|
||||||
|
|
||||||
|
// Then create the local bot account
|
||||||
|
var bot = new BotAccount
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(botAccount.AutomatedId),
|
||||||
|
Slug = slug,
|
||||||
|
ProjectId = project.Id,
|
||||||
|
Project = project,
|
||||||
|
IsActive = botAccount.IsActive,
|
||||||
|
CreatedAt = botAccount.CreatedAt.ToInstant(),
|
||||||
|
UpdatedAt = botAccount.UpdatedAt.ToInstant()
|
||||||
|
};
|
||||||
|
|
||||||
|
db.BotAccounts.Add(bot);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A bot account with this ID already exists in the authentication service.", ex);
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex);
|
||||||
|
}
|
||||||
|
catch (RpcException ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BotAccount> UpdateBotAsync(BotAccount bot, string? slug = null, bool? isActive = null)
|
||||||
|
{
|
||||||
|
var updated = false;
|
||||||
|
if (slug != null && bot.Slug != slug)
|
||||||
|
{
|
||||||
|
bot.Slug = slug;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive.HasValue && bot.IsActive != isActive.Value)
|
||||||
|
{
|
||||||
|
bot.IsActive = isActive.Value;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updated) return bot;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Update the bot account in the Pass service
|
||||||
|
var updateRequest = new UpdateBotAccountRequest
|
||||||
|
{
|
||||||
|
AutomatedId = bot.Id.ToString(),
|
||||||
|
Account = new Shared.Proto.Account
|
||||||
|
{
|
||||||
|
Name = $"bot-{bot.Slug}",
|
||||||
|
Nick = $"Bot {bot.Slug}",
|
||||||
|
UpdatedAt = SystemClock.Instance.GetCurrentInstant().ToTimestamp()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest);
|
||||||
|
var updatedBot = updateResponse.Bot;
|
||||||
|
|
||||||
|
// Update local bot account
|
||||||
|
bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant();
|
||||||
|
bot.IsActive = updatedBot.IsActive;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
|
||||||
|
{
|
||||||
|
throw new Exception("Bot account not found in the authentication service", ex);
|
||||||
|
}
|
||||||
|
catch (RpcException ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteBotAsync(BotAccount bot)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Delete the bot account from the Pass service
|
||||||
|
var deleteRequest = new DeleteBotAccountRequest
|
||||||
|
{
|
||||||
|
AutomatedId = bot.Id.ToString(),
|
||||||
|
Force = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await accountReceiver.DeleteBotAccountAsync(deleteRequest);
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
|
||||||
|
{
|
||||||
|
// Account not found in Pass service, continue with local deletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the local bot account
|
||||||
|
db.BotAccounts.Remove(bot);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
@@ -52,6 +52,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<DeveloperService>();
|
services.AddScoped<DeveloperService>();
|
||||||
services.AddScoped<CustomAppService>();
|
services.AddScoped<CustomAppService>();
|
||||||
services.AddScoped<DevProjectService>();
|
services.AddScoped<DevProjectService>();
|
||||||
|
services.AddScoped<BotAccountService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,9 @@ public class Account : ModelBase
|
|||||||
[MaxLength(32)] public string Language { get; set; } = string.Empty;
|
[MaxLength(32)] public string Language { get; set; } = string.Empty;
|
||||||
public Instant? ActivatedAt { get; set; }
|
public Instant? ActivatedAt { get; set; }
|
||||||
public bool IsSuperuser { get; set; } = false;
|
public bool IsSuperuser { get; set; } = false;
|
||||||
|
|
||||||
|
// The ID is the BotAccount ID in the DysonNetwork.Develop
|
||||||
|
public Guid? AutomatedId { get; set; }
|
||||||
|
|
||||||
public AccountProfile Profile { get; set; } = null!;
|
public AccountProfile Profile { get; set; } = null!;
|
||||||
public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>();
|
public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>();
|
||||||
|
@@ -178,6 +178,29 @@ public class AccountService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Account> CreateBotAccount(Account account, Guid automatedId)
|
||||||
|
{
|
||||||
|
var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync();
|
||||||
|
if (dupeAutomateCount > 0)
|
||||||
|
throw new InvalidOperationException("Automated ID has already been used.");
|
||||||
|
|
||||||
|
var dupeNameCount = await db.Accounts.Where(a => a.Name == account.Name).CountAsync();
|
||||||
|
if (dupeNameCount > 0)
|
||||||
|
throw new InvalidOperationException("Account name has already been taken.");
|
||||||
|
|
||||||
|
account.AutomatedId = automatedId;
|
||||||
|
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
account.IsSuperuser = false;
|
||||||
|
db.Accounts.Add(account);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Account?> GetBotAccount(Guid automatedId)
|
||||||
|
{
|
||||||
|
return await db.Accounts.FirstOrDefaultAsync(a => a.AutomatedId == automatedId);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RequestAccountDeletion(Account account)
|
public async Task RequestAccountDeletion(Account account)
|
||||||
{
|
{
|
||||||
var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
@@ -665,21 +688,13 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task DeleteAccount(Account account)
|
||||||
/// The maintenance method for server administrator.
|
|
||||||
/// To check every user has an account profile and to create them if it isn't having one.
|
|
||||||
/// </summary>
|
|
||||||
public async Task EnsureAccountProfileCreated()
|
|
||||||
{
|
{
|
||||||
var accountsId = await db.Accounts.Select(a => a.Id).ToListAsync();
|
await db.AuthSessions
|
||||||
var existingId = await db.AccountProfiles.Select(p => p.AccountId).ToListAsync();
|
.Where(s => s.AccountId == account.Id)
|
||||||
var missingId = accountsId.Except(existingId).ToList();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
if (missingId.Count != 0)
|
db.Accounts.Remove(account);
|
||||||
{
|
await db.SaveChangesAsync();
|
||||||
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id })
|
|
||||||
.ToList();
|
|
||||||
await db.BulkInsertAsync(newProfiles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
77
DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs
Normal file
77
DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Grpc.Core;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.Protobuf;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts)
|
||||||
|
: BotAccountReceiverService.BotAccountReceiverServiceBase
|
||||||
|
{
|
||||||
|
public override async Task<CreateBotAccountResponse> CreateBotAccount(
|
||||||
|
CreateBotAccountRequest request,
|
||||||
|
ServerCallContext context
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var account = Account.FromProtoValue(request.Account);
|
||||||
|
account = await accounts.CreateBotAccount(account, Guid.Parse(request.AutomatedId));
|
||||||
|
|
||||||
|
return new CreateBotAccountResponse
|
||||||
|
{
|
||||||
|
Bot = new BotAccount
|
||||||
|
{
|
||||||
|
Account = account.ToProtoValue(),
|
||||||
|
AutomatedId = account.Id.ToString(),
|
||||||
|
CreatedAt = account.CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = account.UpdatedAt.ToTimestamp(),
|
||||||
|
IsActive = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<UpdateBotAccountResponse> UpdateBotAccount(
|
||||||
|
UpdateBotAccountRequest request,
|
||||||
|
ServerCallContext context
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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"));
|
||||||
|
|
||||||
|
account.Name = request.Account.Name;
|
||||||
|
account.Nick = request.Account.Nick;
|
||||||
|
account.Profile = AccountProfile.FromProtoValue(request.Account.Profile);
|
||||||
|
account.Language = request.Account.Language;
|
||||||
|
|
||||||
|
db.Accounts.Update(account);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new UpdateBotAccountResponse
|
||||||
|
{
|
||||||
|
Bot = new BotAccount
|
||||||
|
{
|
||||||
|
Account = account.ToProtoValue(),
|
||||||
|
AutomatedId = account.Id.ToString(),
|
||||||
|
CreatedAt = account.CreatedAt.ToTimestamp(),
|
||||||
|
UpdatedAt = account.UpdatedAt.ToTimestamp(),
|
||||||
|
IsActive = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<DeleteBotAccountResponse> DeleteBotAccount(
|
||||||
|
DeleteBotAccountRequest request,
|
||||||
|
ServerCallContext context
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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"));
|
||||||
|
|
||||||
|
await accounts.DeleteAccount(account);
|
||||||
|
|
||||||
|
return new DeleteBotAccountResponse();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,4 @@
|
|||||||
using DysonNetwork.Pass;
|
using DysonNetwork.Pass;
|
||||||
using DysonNetwork.Pass.Auth;
|
|
||||||
using DysonNetwork.Pass.Pages.Data;
|
using DysonNetwork.Pass.Pages.Data;
|
||||||
using DysonNetwork.Pass.Startup;
|
using DysonNetwork.Pass.Startup;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
|
@@ -76,6 +76,7 @@ public static class ApplicationConfiguration
|
|||||||
app.MapGrpcService<AuthServiceGrpc>();
|
app.MapGrpcService<AuthServiceGrpc>();
|
||||||
app.MapGrpcService<ActionLogServiceGrpc>();
|
app.MapGrpcService<ActionLogServiceGrpc>();
|
||||||
app.MapGrpcService<PermissionServiceGrpc>();
|
app.MapGrpcService<PermissionServiceGrpc>();
|
||||||
|
app.MapGrpcService<BotAccountReceiverGrpc>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
@@ -32,12 +32,9 @@ public static class GrpcClientHelper
|
|||||||
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
|
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
|
||||||
{
|
{
|
||||||
var response = await etcdClient.GetAsync($"/services/{serviceName}");
|
var response = await etcdClient.GetAsync($"/services/{serviceName}");
|
||||||
if (response.Kvs.Count == 0)
|
return response.Kvs.Count == 0
|
||||||
{
|
? throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.")
|
||||||
throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.");
|
: response.Kvs[0].Value.ToStringUtf8();
|
||||||
}
|
|
||||||
|
|
||||||
return response.Kvs[0].Value.ToStringUtf8();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
|
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
|
||||||
@@ -51,7 +48,21 @@ public static class GrpcClientHelper
|
|||||||
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<BotAccountReceiverService.BotAccountReceiverServiceClient>
|
||||||
|
CreateBotAccountReceiverServiceClient(
|
||||||
|
IEtcdClient etcdClient,
|
||||||
|
string clientCertPath,
|
||||||
|
string clientKeyPath,
|
||||||
|
string? clientCertPassword = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
|
||||||
|
return new BotAccountReceiverService.BotAccountReceiverServiceClient(CreateCallInvoker(url, clientCertPath,
|
||||||
|
clientKeyPath,
|
||||||
|
clientCertPassword));
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<ActionLogService.ActionLogServiceClient> CreateActionLogServiceClient(
|
public static async Task<ActionLogService.ActionLogServiceClient> CreateActionLogServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@@ -75,7 +86,7 @@ public static class GrpcClientHelper
|
|||||||
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<PermissionService.PermissionServiceClient> CreatePermissionServiceClient(
|
public static async Task<PermissionService.PermissionServiceClient> CreatePermissionServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@@ -99,7 +110,7 @@ public static class GrpcClientHelper
|
|||||||
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<FileService.FileServiceClient> CreateFileServiceClient(
|
public static async Task<FileService.FileServiceClient> CreateFileServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@@ -111,7 +122,7 @@ public static class GrpcClientHelper
|
|||||||
return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<FileReferenceService.FileReferenceServiceClient> CreateFileReferenceServiceClient(
|
public static async Task<FileReferenceService.FileReferenceServiceClient> CreateFileReferenceServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@@ -123,7 +134,7 @@ public static class GrpcClientHelper
|
|||||||
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
|
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@@ -147,4 +158,4 @@ public static class GrpcClientHelper
|
|||||||
return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,6 +5,7 @@ package proto;
|
|||||||
option csharp_namespace = "DysonNetwork.Shared.Proto";
|
option csharp_namespace = "DysonNetwork.Shared.Proto";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "account.proto";
|
||||||
import "file.proto";
|
import "file.proto";
|
||||||
|
|
||||||
message CustomAppOauthConfig {
|
message CustomAppOauthConfig {
|
||||||
@@ -25,7 +26,7 @@ enum CustomAppStatus {
|
|||||||
SUSPENDED = 4;
|
SUSPENDED = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CustomApp {
|
message CustomApp {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string slug = 2;
|
string slug = 2;
|
||||||
string name = 3;
|
string name = 3;
|
||||||
@@ -43,7 +44,7 @@ enum CustomAppStatus {
|
|||||||
|
|
||||||
google.protobuf.Timestamp created_at = 11;
|
google.protobuf.Timestamp created_at = 11;
|
||||||
google.protobuf.Timestamp updated_at = 12;
|
google.protobuf.Timestamp updated_at = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CustomAppSecret {
|
message CustomAppSecret {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
@@ -85,4 +86,61 @@ message CheckCustomAppSecretResponse {
|
|||||||
service CustomAppService {
|
service CustomAppService {
|
||||||
rpc GetCustomApp(GetCustomAppRequest) returns (GetCustomAppResponse);
|
rpc GetCustomApp(GetCustomAppRequest) returns (GetCustomAppResponse);
|
||||||
rpc CheckCustomAppSecret(CheckCustomAppSecretRequest) returns (CheckCustomAppSecretResponse);
|
rpc CheckCustomAppSecret(CheckCustomAppSecretRequest) returns (CheckCustomAppSecretResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BotAccount represents a bot account in the system
|
||||||
|
// It extends the base Account with bot-specific fields
|
||||||
|
message BotAccount {
|
||||||
|
// Base account information
|
||||||
|
Account account = 1;
|
||||||
|
|
||||||
|
// Bot-specific information
|
||||||
|
string slug = 2; // Unique identifier for the bot
|
||||||
|
bool is_active = 3; // Whether the bot is currently active
|
||||||
|
string automated_id = 5; // The bot ID
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
google.protobuf.Timestamp created_at = 6;
|
||||||
|
google.protobuf.Timestamp updated_at = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request/Response messages for BotAccount operations
|
||||||
|
message CreateBotAccountRequest {
|
||||||
|
Account account = 1;
|
||||||
|
string automated_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateBotAccountResponse {
|
||||||
|
BotAccount bot = 1; // The created bot account
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateBotAccountRequest {
|
||||||
|
string automated_id = 1; // ID of the bot account to update
|
||||||
|
Account account = 2; // Updated account information
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateBotAccountResponse {
|
||||||
|
BotAccount bot = 1; // The updated bot account
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteBotAccountRequest {
|
||||||
|
string automated_id = 1; // ID of the bot account to delete
|
||||||
|
bool force = 2; // Whether to force delete (bypass soft delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteBotAccountResponse {
|
||||||
|
bool success = 1; // Whether the deletion was successful
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
@@ -43,6 +43,20 @@ public static class ServiceInjectionHelper
|
|||||||
});
|
});
|
||||||
services.AddSingleton<AccountClientHelper>();
|
services.AddSingleton<AccountClientHelper>();
|
||||||
|
|
||||||
|
services.AddSingleton<BotAccountReceiverService.BotAccountReceiverServiceClient>(sp =>
|
||||||
|
{
|
||||||
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var clientCertPath = config["Service:ClientCert"]!;
|
||||||
|
var clientKeyPath = config["Service:ClientKey"]!;
|
||||||
|
var clientCertPassword = config["Service:CertPassword"];
|
||||||
|
|
||||||
|
return GrpcClientHelper
|
||||||
|
.CreateBotAccountReceiverServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
});
|
||||||
|
|
||||||
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
|
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
|
||||||
{
|
{
|
||||||
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
|
Reference in New Issue
Block a user