297 lines
11 KiB
C#
297 lines
11 KiB
C#
using System.Text.Json;
|
|
using RelayServer.Models;
|
|
using RelayServer.Services.Chat;
|
|
using RelayServer.Services.Crypto;
|
|
using RelayShared.Services;
|
|
using SurrealDb.Net;
|
|
|
|
namespace RelayServer.Services.Core;
|
|
|
|
public sealed class ServerBootstrapService
|
|
{
|
|
private readonly SurrealDbClient _db;
|
|
private readonly CoreClientService _coreClient;
|
|
private readonly ChannelCryptoService _cryptoService;
|
|
|
|
public ServerBootstrapService(
|
|
SurrealDbClient db,
|
|
CoreClientService coreClient,
|
|
ChannelCryptoService cryptoService)
|
|
{
|
|
_db = db;
|
|
_coreClient = coreClient;
|
|
_cryptoService = cryptoService;
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
var keeper = await _coreClient.GetUserByUsernameAsync("Keeper317");
|
|
var kira = await _coreClient.GetUserByUsernameAsync("Ru_Kira");
|
|
var test = await _coreClient.GetUserByUsernameAsync("Test");
|
|
|
|
if (keeper is null || kira is null || test is null)
|
|
throw new InvalidOperationException("One or more required users do not exist in RelayCore.");
|
|
|
|
if (!keeper.Licensed || !kira.Licensed || !test.Licensed)
|
|
throw new InvalidOperationException("One or more required users are not licensed.");
|
|
|
|
Console.WriteLine($"Core verified: {keeper.Username}, {kira.Username}, {test.Username}");
|
|
|
|
var server = await GetServerByNameAsync("Test Server");
|
|
|
|
if (server is null)
|
|
{
|
|
server = await _db.Create("servers", new Servers
|
|
{
|
|
Name = "Test Server",
|
|
OwnerUserId = keeper.Id,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
Console.WriteLine($"Server created: {ToJson(server)}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Server already exists: {server.Name}");
|
|
}
|
|
|
|
await EnsureServerMemberAsync(keeper.Id, isOwner: true);
|
|
await EnsureServerMemberAsync(kira.Id, isOwner: false);
|
|
await EnsureServerMemberAsync(test.Id, isOwner: false);
|
|
Console.WriteLine("Server members ensured.");
|
|
|
|
var tBase = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
var chWelcome = await EnsureChannelAsync("welcome", ChannelType.Text, group: "General", isReadOnly: true, createdAt: tBase);
|
|
var chGeneral = await EnsureChannelAsync("general", ChannelType.Text, group: "General", isReadOnly: false, createdAt: tBase.AddHours(1));
|
|
var chFiles = await EnsureChannelAsync("files", ChannelType.File, group: "General", isReadOnly: true, createdAt: tBase.AddHours(2));
|
|
var chVoice = await EnsureChannelAsync("voice-general", ChannelType.Voice, group: "General", isReadOnly: false, createdAt: tBase.AddHours(3));
|
|
|
|
Console.WriteLine($"Channels: {GetRecordId(chWelcome.Id)} | {GetRecordId(chGeneral.Id)} | {GetRecordId(chFiles.Id)} | {GetRecordId(chVoice.Id)}");
|
|
|
|
await EnsureFileChannelLinkAsync(chGeneral, GetRecordId(chFiles.Id));
|
|
|
|
var adminRole = await EnsureRoleAsync("Admin", PermissionFlags.Administrator, priority: 0);
|
|
var modRole = await EnsureRoleAsync("Moderator", PermissionFlags.ReadMessages | PermissionFlags.SendMessages | PermissionFlags.ManageMessages, priority: 1);
|
|
var memberRole = await EnsureRoleAsync("Member", PermissionFlags.ReadMessages | PermissionFlags.SendMessages, priority: 2);
|
|
|
|
Console.WriteLine($"Roles ensured: Admin={GetRecordId(adminRole.Id)}, Mod={GetRecordId(modRole.Id)}, Member={GetRecordId(memberRole.Id)}");
|
|
|
|
await SetUserRoleAsync(keeper.Id, GetRecordId(adminRole.Id));
|
|
await SetUserRoleAsync(kira.Id, GetRecordId(modRole.Id));
|
|
await SetUserRoleAsync(test.Id, GetRecordId(memberRole.Id));
|
|
Console.WriteLine("User roles set.");
|
|
|
|
await EnsureChannelPermissionAsync(GetRecordId(chWelcome.Id), GetRecordId(memberRole.Id),
|
|
allow: PermissionFlags.ReadMessages, deny: PermissionFlags.SendMessages);
|
|
await EnsureChannelPermissionAsync(GetRecordId(chFiles.Id), GetRecordId(memberRole.Id),
|
|
allow: PermissionFlags.ReadMessages, deny: PermissionFlags.SendMessages);
|
|
|
|
Console.WriteLine("Channel permissions ensured.");
|
|
|
|
var existingKey = await GetLatestServerEncryptionKeyAsync();
|
|
|
|
if (existingKey is null)
|
|
{
|
|
var keyBase64 = _cryptoService.GenerateKey();
|
|
var serverKeys = E2EeHelper.GenerateRsaKeyPair();
|
|
|
|
existingKey = await _db.Create("server_encryption_keys", new ServerEncryptionKeys
|
|
{
|
|
KeyBase64 = keyBase64,
|
|
PublicKey = serverKeys.publicKey,
|
|
PrivateKey = serverKeys.privateKey,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
});
|
|
|
|
Console.WriteLine("Server encryption key created.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Server encryption key already exists.");
|
|
}
|
|
|
|
ChatSocketBehavior.ServerPublicKey = existingKey.PublicKey;
|
|
ChatSocketBehavior.ServerPrivateKey = existingKey.PrivateKey;
|
|
ChatSocketBehavior.ChannelDbKey = existingKey.KeyBase64;
|
|
}
|
|
|
|
private async Task EnsureServerMemberAsync(string userId, bool isOwner)
|
|
{
|
|
var members = await _db.Select<ServerMembers>("server_members");
|
|
var existing = members.FirstOrDefault(m => m.UserId == userId);
|
|
|
|
if (existing is not null)
|
|
{
|
|
if (existing.IsOwner != isOwner)
|
|
{
|
|
existing.IsOwner = isOwner;
|
|
await _db.Merge<ServerMembers, ServerMembers>(existing);
|
|
Console.WriteLine($"Member IsOwner updated: {userId} → {isOwner}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Member already correct: {userId}");
|
|
}
|
|
return;
|
|
}
|
|
|
|
await _db.Create("server_members", new ServerMembers
|
|
{
|
|
UserId = userId,
|
|
JoinedAt = DateTime.UtcNow,
|
|
IsOwner = isOwner
|
|
});
|
|
Console.WriteLine($"Member created: {userId} (IsOwner={isOwner})");
|
|
}
|
|
|
|
private async Task<Channels> EnsureChannelAsync(
|
|
string name, ChannelType type, string group, bool isReadOnly, DateTime createdAt)
|
|
{
|
|
var channels = await _db.Select<Channels>("channels");
|
|
var existing = channels.FirstOrDefault(c => c.Name == name);
|
|
|
|
if (existing is not null)
|
|
{
|
|
bool dirty = existing.Type != type || existing.Group != group || existing.IsReadOnly != isReadOnly;
|
|
if (dirty)
|
|
{
|
|
existing.Type = type;
|
|
existing.Group = group;
|
|
existing.IsReadOnly = isReadOnly;
|
|
await _db.Merge<Channels, Channels>(existing);
|
|
Console.WriteLine($"Channel updated: {name}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Channel already correct: {name}");
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
var channel = await _db.Create("channels", new Channels
|
|
{
|
|
Name = name,
|
|
Type = type,
|
|
Group = group,
|
|
IsReadOnly = isReadOnly,
|
|
CreatedAt = createdAt
|
|
});
|
|
|
|
Console.WriteLine($"Channel created: {name} ({type})");
|
|
return channel;
|
|
}
|
|
|
|
private async Task EnsureFileChannelLinkAsync(Channels channel, string fileChannelId)
|
|
{
|
|
if (channel.LinkedFileChannelId == fileChannelId)
|
|
{
|
|
Console.WriteLine($"File link already correct: {channel.Name} → {fileChannelId}");
|
|
return;
|
|
}
|
|
|
|
channel.LinkedFileChannelId = fileChannelId;
|
|
await _db.Merge<Channels, Channels>(channel);
|
|
Console.WriteLine($"File link set: {channel.Name} → {fileChannelId}");
|
|
}
|
|
|
|
private async Task<Roles> EnsureRoleAsync(string name, PermissionFlags permissions, int priority)
|
|
{
|
|
var roles = await _db.Select<Roles>("roles");
|
|
var existing = roles.FirstOrDefault(r => r.Name == name);
|
|
|
|
if (existing is not null)
|
|
{
|
|
Console.WriteLine($"Role already exists: {name}");
|
|
return existing;
|
|
}
|
|
|
|
var role = await _db.Create("roles", new Roles
|
|
{
|
|
Name = name,
|
|
Permissions = permissions,
|
|
Priority = priority,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
Console.WriteLine($"Role created: {name}");
|
|
return role;
|
|
}
|
|
|
|
private async Task SetUserRoleAsync(string userId, string roleId)
|
|
{
|
|
var userRoles = await _db.Select<UserRoles>("user_roles");
|
|
var existing = userRoles
|
|
.Where(ur => string.Equals(ur.UserId, userId, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
bool alreadyCorrect = existing.Count == 1 && existing[0].RoleId == roleId;
|
|
if (alreadyCorrect)
|
|
{
|
|
Console.WriteLine($"UserRole already correct: {userId} → {roleId}");
|
|
return;
|
|
}
|
|
|
|
foreach (var stale in existing)
|
|
{
|
|
if (stale.Id is not null)
|
|
await _db.Delete(stale.Id);
|
|
}
|
|
|
|
await _db.Create("user_roles", new UserRoles
|
|
{
|
|
UserId = userId,
|
|
RoleId = roleId,
|
|
AssignedAt = DateTime.UtcNow
|
|
});
|
|
Console.WriteLine($"UserRole set: {userId} → {roleId}");
|
|
}
|
|
|
|
private async Task EnsureChannelPermissionAsync(
|
|
string channelId, string roleId, PermissionFlags allow, PermissionFlags deny)
|
|
{
|
|
var perms = await _db.Select<ChannelPermissions>("channel_permissions");
|
|
if (perms.Any(cp => cp.ChannelId == channelId && cp.RoleId == roleId))
|
|
{
|
|
Console.WriteLine($"ChannelPermission already exists: {channelId} → {roleId}");
|
|
return;
|
|
}
|
|
|
|
await _db.Create("channel_permissions", new ChannelPermissions
|
|
{
|
|
ChannelId = channelId,
|
|
RoleId = roleId,
|
|
Allow = allow,
|
|
Deny = deny
|
|
});
|
|
Console.WriteLine($"ChannelPermission created: {channelId} → {roleId} | allow={allow}, deny={deny}");
|
|
}
|
|
|
|
private async Task<Servers?> GetServerByNameAsync(string name)
|
|
{
|
|
var servers = await _db.Select<Servers>("servers");
|
|
return servers.FirstOrDefault(x => x.Name == name);
|
|
}
|
|
|
|
private async Task<ServerEncryptionKeys?> GetLatestServerEncryptionKeyAsync()
|
|
{
|
|
var keys = await _db.Select<ServerEncryptionKeys>("server_encryption_keys");
|
|
return keys.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
|
|
}
|
|
|
|
private static string GetRecordId(object? id)
|
|
{
|
|
if (id is null) return string.Empty;
|
|
var json = JsonSerializer.Serialize(id);
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
return $"{root.GetProperty("Table").GetString()}:{root.GetProperty("Id").GetString()}";
|
|
}
|
|
|
|
private static string ToJson(object? obj) =>
|
|
JsonSerializer.Serialize(obj, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
});
|
|
}
|