Local changes
This commit is contained in:
parent
a632666203
commit
abb1b570a4
27
.idea/runConfigurations/Start_API.xml
generated
Normal file
27
.idea/runConfigurations/Start_API.xml
generated
Normal file
@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Start API" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start:api" />
|
||||
</scripts>
|
||||
<node-interpreter value="/nix/store/dcdc33kdjdhjnzg6rkmd0cx4kpwl8cac-nodejs-20.17.0/bin/node" />
|
||||
<package-manager value="npm" />
|
||||
<envs />
|
||||
<EXTENSION ID="com.fapiko.jetbrains.plugins.better_direnv.runconfigs.NodeRunConfiguration">
|
||||
<option name="DIRENV_ENABLED" value="false" />
|
||||
<option name="DIRENV_TRUSTED" value="false" />
|
||||
</EXTENSION>
|
||||
<method v="2">
|
||||
<option name="NpmBeforeRunTask" enabled="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
</option>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
27
.idea/runConfigurations/Start_CDN.xml
generated
Normal file
27
.idea/runConfigurations/Start_CDN.xml
generated
Normal file
@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Start CDN" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start:cdn" />
|
||||
</scripts>
|
||||
<node-interpreter value="/nix/store/dcdc33kdjdhjnzg6rkmd0cx4kpwl8cac-nodejs-20.17.0/bin/node" />
|
||||
<package-manager value="npm" />
|
||||
<envs />
|
||||
<EXTENSION ID="com.fapiko.jetbrains.plugins.better_direnv.runconfigs.NodeRunConfiguration">
|
||||
<option name="DIRENV_ENABLED" value="false" />
|
||||
<option name="DIRENV_TRUSTED" value="false" />
|
||||
</EXTENSION>
|
||||
<method v="2">
|
||||
<option name="NpmBeforeRunTask" enabled="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
</option>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
27
.idea/runConfigurations/Start_Gateway.xml
generated
Normal file
27
.idea/runConfigurations/Start_Gateway.xml
generated
Normal file
@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Start Gateway" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start:gateway" />
|
||||
</scripts>
|
||||
<node-interpreter value="/nix/store/dcdc33kdjdhjnzg6rkmd0cx4kpwl8cac-nodejs-20.17.0/bin/node" />
|
||||
<package-manager value="npm" />
|
||||
<envs />
|
||||
<EXTENSION ID="com.fapiko.jetbrains.plugins.better_direnv.runconfigs.NodeRunConfiguration">
|
||||
<option name="DIRENV_ENABLED" value="false" />
|
||||
<option name="DIRENV_TRUSTED" value="false" />
|
||||
</EXTENSION>
|
||||
<method v="2">
|
||||
<option name="NpmBeforeRunTask" enabled="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
</option>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true">
|
||||
<configuration default="false" name="Start bundle" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
8
.idea/runConfigurations/Start_separated.xml
generated
Normal file
8
.idea/runConfigurations/Start_separated.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Start separated" type="CompoundRunConfigurationType">
|
||||
<toRun name="Start API" type="js.build_tools.npm" />
|
||||
<toRun name="Start CDN" type="js.build_tools.npm" />
|
||||
<toRun name="Start Gateway" type="js.build_tools.npm" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
69
.idea/workspace.xml
generated
69
.idea/workspace.xml
generated
@ -11,8 +11,12 @@
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$USER_HOME$/.cache/JetBrains/WebStorm2024.3/javascript/typings/node/20.17.10/node_modules/@types/node/crypto.d.ts" root0="SKIP_INSPECTION" />
|
||||
<setting file="file://$PROJECT_DIR$/node_modules/@types/amqplib/index.d.ts" root0="SKIP_INSPECTION" />
|
||||
<setting file="file://$PROJECT_DIR$/node_modules/@types/jsonwebtoken/index.d.ts" root0="SKIP_INSPECTION" />
|
||||
<setting file="file://$PROJECT_DIR$/node_modules/@types/node/events.d.ts" root0="SKIP_INSPECTION" />
|
||||
<setting file="file://$PROJECT_DIR$/result/node_modules/lambert-server/src/Server.ts" root0="SKIP_INSPECTION" />
|
||||
<setting file="file://$PROJECT_DIR$/result/node_modules/lambert-server/src/Utils.ts" root0="SKIP_INSPECTION" />
|
||||
</component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="ProjectErrors" />
|
||||
@ -26,32 +30,35 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix",
|
||||
"NIXITCH_NIX_CONF_DIR": "",
|
||||
"NIXITCH_NIX_OTHER_STORES": "",
|
||||
"NIXITCH_NIX_PATH": "/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source",
|
||||
"NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile",
|
||||
"NIXITCH_NIX_REMOTE": "",
|
||||
"NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/Rory",
|
||||
"Node.js.Server.ts.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"javascript.nodejs.core.library.configured.version": "20.17.0",
|
||||
"javascript.nodejs.core.library.typings.version": "20.17.10",
|
||||
"last_opened_file_path": "/home/Rory/git/spacebar/server/src/admin-api/routes/v0",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_interpreter_path": "/nix/store/dcdc33kdjdhjnzg6rkmd0cx4kpwl8cac-nodejs-20.17.0/bin/node",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.start.executor": "Debug",
|
||||
"prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server/node_modules/prettier",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "/home/Rory/git/spacebar/server/node_modules/typescript/lib"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix",
|
||||
"NIXITCH_NIX_CONF_DIR": "",
|
||||
"NIXITCH_NIX_OTHER_STORES": "",
|
||||
"NIXITCH_NIX_PATH": "/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source",
|
||||
"NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile",
|
||||
"NIXITCH_NIX_REMOTE": "",
|
||||
"NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/Rory",
|
||||
"Node.js.Server.ts.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"javascript.nodejs.core.library.configured.version": "20.17.0",
|
||||
"javascript.nodejs.core.library.typings.version": "20.17.10",
|
||||
"last_opened_file_path": "/home/Rory/git/spacebar/server/src/admin-api/routes/v0",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_interpreter_path": "/nix/store/dcdc33kdjdhjnzg6rkmd0cx4kpwl8cac-nodejs-20.17.0/bin/node",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.Start API.executor": "Run",
|
||||
"npm.Start CDN.executor": "Run",
|
||||
"npm.Start Gateway.executor": "Run",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.start.executor": "Debug",
|
||||
"prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server/node_modules/prettier",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "/home/Rory/git/spacebar/server/node_modules/typescript/lib"
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/admin-api/routes/v0" />
|
||||
@ -66,6 +73,15 @@
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager" selected="Compound.Start separated">
|
||||
<list>
|
||||
<item itemvalue="Compound.Start separated" />
|
||||
<item itemvalue="npm.Start bundle" />
|
||||
<item itemvalue="npm.Start API" />
|
||||
<item itemvalue="npm.Start CDN" />
|
||||
<item itemvalue="npm.Start Gateway" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
@ -90,7 +106,8 @@
|
||||
<workItem from="1735163422404" duration="28000" />
|
||||
<workItem from="1735163530041" duration="25785000" />
|
||||
<workItem from="1735256039333" duration="390000" />
|
||||
<workItem from="1735256441657" duration="15485000" />
|
||||
<workItem from="1735256441657" duration="58156000" />
|
||||
<workItem from="1735848116134" duration="80763000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@ -25,6 +25,7 @@ This repository contains:
|
||||
- [WebSocket Gateway Server](/src/gateway)
|
||||
- [HTTP CDN Server](/src/cdn)
|
||||
- [Utility and Database Models](/src/util)
|
||||
- [Spacebar Admin API](/extra/admin-api) (Emma [it/its]@Rory& was here)
|
||||
|
||||
## [Documentation](https://docs.spacebar.chat)
|
||||
|
||||
|
||||
7
extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
generated
Normal file
7
extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/Spacebar.AdminAPI/Controllers/UserController.cs" dialect="GenericSQL" />
|
||||
<file url="PROJECT" dialect="PostgreSQL" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Spacebar.AdminAPI.Services;
|
||||
|
||||
namespace Spacebar.AdminAPI.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("/")]
|
||||
public class PingController(ILogger<PingController> logger, IServiceProvider sp, AuthenticationService auth) : ControllerBase {
|
||||
private readonly ILogger<PingController> _logger = logger;
|
||||
|
||||
[HttpGet("ping")]
|
||||
public async Task<object> Ping() {
|
||||
return new {
|
||||
ok = true
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("whoami")]
|
||||
public async Task<object> WhoAmI() {
|
||||
var user = await auth.GetCurrentUser(Request);
|
||||
return new {
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.Discriminator,
|
||||
user.Bot,
|
||||
user.Flags,
|
||||
user.Rights,
|
||||
user.MfaEnabled,
|
||||
user.WebauthnEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,404 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using ArcaneLibs.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RabbitMQ.Client;
|
||||
using Spacebar.AdminApi.Models;
|
||||
using Spacebar.Db.Contexts;
|
||||
using Spacebar.Db.Models;
|
||||
using Spacebar.RabbitMqUtilities;
|
||||
|
||||
namespace Spacebar.AdminAPI.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("/users")]
|
||||
public class UserController(ILogger<UserController> logger, SpacebarDbContext db) : ControllerBase {
|
||||
[Route("/Users")]
|
||||
public class UserController(ILogger<UserController> logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase {
|
||||
private readonly ILogger<UserController> _logger = logger;
|
||||
|
||||
[HttpGet(Name = "/")]
|
||||
public IAsyncEnumerable<User> Get() {
|
||||
return db.Users.AsAsyncEnumerable();
|
||||
[HttpGet]
|
||||
public IAsyncEnumerable<UserModel> Get() {
|
||||
return db.Users.Select(x => new UserModel {
|
||||
Id = x.Id,
|
||||
Username = x.Username,
|
||||
Discriminator = x.Discriminator,
|
||||
Avatar = x.Avatar,
|
||||
AccentColor = x.AccentColor,
|
||||
Banner = x.Banner,
|
||||
ThemeColors = x.ThemeColors,
|
||||
Pronouns = x.Pronouns,
|
||||
Phone = x.Phone,
|
||||
Desktop = x.Desktop,
|
||||
Mobile = x.Mobile,
|
||||
Premium = x.Premium,
|
||||
PremiumType = x.PremiumType,
|
||||
Bot = x.Bot,
|
||||
Bio = x.Bio,
|
||||
System = x.System,
|
||||
NsfwAllowed = x.NsfwAllowed,
|
||||
MfaEnabled = x.MfaEnabled,
|
||||
WebauthnEnabled = x.WebauthnEnabled,
|
||||
CreatedAt = x.CreatedAt,
|
||||
PremiumSince = x.PremiumSince,
|
||||
Verified = x.Verified,
|
||||
Disabled = x.Disabled,
|
||||
Deleted = x.Deleted,
|
||||
Email = x.Email,
|
||||
Flags = x.Flags,
|
||||
PublicFlags = x.PublicFlags,
|
||||
Rights = x.Rights,
|
||||
ApplicationBotUser = x.ApplicationBotUser == null ? null : new() { },
|
||||
ConnectedAccounts = new List<UserModel.ConnectedAccountModel>(),
|
||||
MessageCount = x.MessageAuthors.Count, // This property is weirdly named due to scaffolding, might patch later
|
||||
SessionCount = x.Sessions.Count,
|
||||
TemplateCount = x.Templates.Count,
|
||||
VoiceStateCount = x.VoiceStates.Count,
|
||||
GuildCount = x.Guilds.Count,
|
||||
OwnedGuildCount = x.Guilds.Count(g => g.OwnerId == x.Id)
|
||||
}).AsAsyncEnumerable();
|
||||
}
|
||||
|
||||
[HttpGet("meow")]
|
||||
public async Task Meow() {
|
||||
Console.WriteLine("meow");
|
||||
|
||||
ConnectionFactory factory = new ConnectionFactory();
|
||||
factory.Uri = new Uri("amqp://guest:guest@127.0.0.1/");
|
||||
using var connection = await factory.CreateConnectionAsync();
|
||||
using var channel = await connection.CreateChannelAsync();
|
||||
|
||||
// await using var channel = mq.CreateChannel();
|
||||
// var channel2 = await channel.CreateChannelAsync();
|
||||
|
||||
var body =
|
||||
$$"""
|
||||
{
|
||||
"id": "{{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}}",
|
||||
"channel_id": "1322343566206308390",
|
||||
"guild_id": "1322343566084673571",
|
||||
"author": {
|
||||
"username": "test",
|
||||
"discriminator": "9177",
|
||||
"id": "1322329228934500382",
|
||||
"public_flags": 0,
|
||||
"avatar": null,
|
||||
"accent_color": null,
|
||||
"banner": null,
|
||||
"bio": "",
|
||||
"bot": false,
|
||||
"premium_since": "2024-12-27T22:24:15.867Z",
|
||||
"premium_type": 2,
|
||||
"theme_colors": null,
|
||||
"pronouns": null,
|
||||
"badge_ids": null
|
||||
},
|
||||
"member": {
|
||||
"index": 2,
|
||||
"id": "1322329228934500382",
|
||||
"guild_id": "1322343566084673571",
|
||||
"nick": null,
|
||||
"joined_at": "2024-12-27T23:21:14.396Z",
|
||||
"premium_since": null,
|
||||
"deaf": false,
|
||||
"mute": false,
|
||||
"pending": false,
|
||||
"last_message_id": "1322346635635753061",
|
||||
"joined_by": null,
|
||||
"avatar": null,
|
||||
"banner": null,
|
||||
"bio": "",
|
||||
"theme_colors": null,
|
||||
"pronouns": null,
|
||||
"communication_disabled_until": null,
|
||||
"roles": []
|
||||
},
|
||||
"content": "{{Random.Shared.NextInt64()}}",
|
||||
"timestamp": "{{DateTime.UtcNow:O}}",
|
||||
"edited_timestamp": null,
|
||||
"tts": false,
|
||||
"mention_everyone": false,
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"nonce": "{{Random.Shared.NextInt64()}}",
|
||||
"pinned": false,
|
||||
"type": 0
|
||||
}
|
||||
"""
|
||||
.AsBytes().ToArray();
|
||||
|
||||
await channel.ExchangeDeclareAsync(exchange: "1322343566206308390", type: ExchangeType.Fanout, durable: false);
|
||||
var props = new BasicProperties() { Type = "MESSAGE_CREATE" };
|
||||
await channel.BasicPublishAsync(exchange: "1322343566206308390", routingKey: "", mandatory: true, basicProperties: props, body: body);
|
||||
|
||||
await channel.CloseAsync();
|
||||
await connection.CloseAsync();
|
||||
Console.WriteLine("meowww");
|
||||
}
|
||||
|
||||
[HttpGet("{id}/delete")]
|
||||
public async IAsyncEnumerable<AsyncActionResult> DeleteUser(string id, [FromQuery] int messageDeleteChunkSize = 100) {
|
||||
var user = await db.Users.FindAsync(id);
|
||||
if (user == null) {
|
||||
Console.WriteLine($"User {id} not found");
|
||||
yield return new AsyncActionResult("ERROR", new { entity = "User", id, message = "User not found" });
|
||||
yield break;
|
||||
}
|
||||
|
||||
var factory = new ConnectionFactory {
|
||||
Uri = new Uri("amqp://guest:guest@127.0.0.1/")
|
||||
};
|
||||
await using var mqConnection = await factory.CreateConnectionAsync();
|
||||
await using var mqChannel = await mqConnection.CreateChannelAsync();
|
||||
|
||||
var messages = db.Messages
|
||||
.AsNoTracking()
|
||||
.Where(m => m.AuthorId == id);
|
||||
var channels = messages
|
||||
.Select(m => new { m.ChannelId, m.GuildId })
|
||||
.Distinct()
|
||||
.ToList();
|
||||
yield return new("STATS",
|
||||
new {
|
||||
total_messages = messages.Count(), total_channels = channels.Count,
|
||||
messages_per_channel = channels.ToDictionary(c => c.ChannelId, c => messages.Count(m => m.ChannelId == c.ChannelId))
|
||||
});
|
||||
var results = channels
|
||||
.Select(ctx => DeleteMessagesForChannel(ctx.GuildId, ctx.ChannelId!, id, mqChannel, messageDeleteChunkSize))
|
||||
.ToList();
|
||||
var a = AggregateAsyncEnumerablesWithoutOrder(results);
|
||||
await foreach (var result in a) {
|
||||
yield return result;
|
||||
}
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||
await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages");
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<AsyncActionResult> DeleteMessagesForChannel(
|
||||
// context
|
||||
string? guildId, string channelId, string authorId,
|
||||
// connections
|
||||
IChannel mqChannel,
|
||||
// options
|
||||
int messageDeleteChunkSize = 100
|
||||
) {
|
||||
{
|
||||
await using var ctx = sp.CreateAsyncScope();
|
||||
await using var _db = ctx.ServiceProvider.GetRequiredService<SpacebarDbContext>();
|
||||
await mqChannel.ExchangeDeclareAsync(exchange: channelId!, type: ExchangeType.Fanout, durable: false);
|
||||
var messagesInChannel = _db.Messages.AsNoTracking().Count(m => m.AuthorId == authorId && m.ChannelId == channelId && m.GuildId == guildId);
|
||||
var remaining = messagesInChannel;
|
||||
while (true) {
|
||||
var messageIds = _db.Database.SqlQuery<string>($"""
|
||||
DELETE FROM messages
|
||||
WHERE id IN (
|
||||
SELECT id FROM messages
|
||||
WHERE author_id = {authorId}
|
||||
AND channel_id = {channelId}
|
||||
AND guild_id = {guildId}
|
||||
LIMIT {messageDeleteChunkSize}
|
||||
) RETURNING id;
|
||||
""").ToList();
|
||||
if (messageIds.Count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
var props = new BasicProperties() { Type = "MESSAGE_BULK_DELETE" };
|
||||
var publishSuccess = false;
|
||||
do {
|
||||
try {
|
||||
await mqChannel.BasicPublishAsync(exchange: channelId!, routingKey: "", mandatory: true, basicProperties: props, body: new {
|
||||
ids = messageIds,
|
||||
channel_id = channelId,
|
||||
guild_id = guildId,
|
||||
}.ToJson().AsBytes().ToArray());
|
||||
publishSuccess = true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine($"[RabbitMQ] Error publishing bulk delete: {e.Message}");
|
||||
await Task.Delay(10);
|
||||
}
|
||||
} while (!publishSuccess);
|
||||
|
||||
yield return new("BULK_DELETED", new {
|
||||
channel_id = channelId,
|
||||
total = messagesInChannel,
|
||||
deleted = messageIds.Count,
|
||||
remaining = remaining -= messageIds.Count,
|
||||
});
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("duplicate")]
|
||||
public async Task<IActionResult> Duplicate() {
|
||||
var msg = db.Messages.First();
|
||||
var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList();
|
||||
int count = 1;
|
||||
while (true) {
|
||||
foreach (var channel in channels) {
|
||||
var newMsg = new Message {
|
||||
Id = $"{Random.Shared.NextInt64()}",
|
||||
ChannelId = channel.Id,
|
||||
GuildId = channel.GuildId,
|
||||
AuthorId = msg.AuthorId,
|
||||
Content = msg.Content,
|
||||
MemberId = msg.MemberId,
|
||||
Timestamp = msg.Timestamp,
|
||||
EditedTimestamp = msg.EditedTimestamp,
|
||||
Tts = msg.Tts,
|
||||
MentionEveryone = msg.MentionEveryone,
|
||||
Attachments = msg.Attachments,
|
||||
Embeds = msg.Embeds,
|
||||
Reactions = msg.Reactions,
|
||||
Nonce = msg.Nonce,
|
||||
Pinned = msg.Pinned,
|
||||
Type = msg.Type,
|
||||
};
|
||||
db.Messages.Add(newMsg);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count % 100 == 0) {
|
||||
await db.SaveChangesAsync();
|
||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||
}
|
||||
|
||||
if (count >= 100_000) {
|
||||
await db.SaveChangesAsync();
|
||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||
await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("duplicate/{id}")]
|
||||
public async Task<IActionResult> DuplicateMessage(ulong id, [FromQuery] int count = 100) {
|
||||
var msg = await db.Messages.FindAsync(id.ToString());
|
||||
int createdCount = 1;
|
||||
while (true) {
|
||||
var newMsg = new Message {
|
||||
Id = $"{Random.Shared.NextInt64()}",
|
||||
ChannelId = msg.ChannelId,
|
||||
GuildId = msg.GuildId,
|
||||
AuthorId = msg.AuthorId,
|
||||
Content = msg.Content,
|
||||
MemberId = msg.MemberId,
|
||||
Timestamp = msg.Timestamp,
|
||||
EditedTimestamp = msg.EditedTimestamp,
|
||||
Tts = msg.Tts,
|
||||
MentionEveryone = msg.MentionEveryone,
|
||||
Attachments = msg.Attachments,
|
||||
Embeds = msg.Embeds,
|
||||
Reactions = msg.Reactions,
|
||||
Nonce = msg.Nonce,
|
||||
Pinned = msg.Pinned,
|
||||
Type = msg.Type,
|
||||
};
|
||||
db.Messages.Add(newMsg);
|
||||
createdCount++;
|
||||
|
||||
if (createdCount % 100 == 0) {
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (createdCount >= count) {
|
||||
await db.SaveChangesAsync();
|
||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||
await db.Database.ExecuteSqlRawAsync("REINDEX TABLE messages");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await db.Database.ExecuteSqlRawAsync("VACUUM FULL messages");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("truncate_messages")]
|
||||
public async Task TruncateMessages() {
|
||||
var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList();
|
||||
|
||||
var ss = new SemaphoreSlim(12, 12);
|
||||
|
||||
async Task TruncateChannelMessages(string channelId, string guildId) {
|
||||
await ss.WaitAsync();
|
||||
var tasks = Enumerable.Range(0, 99).Select(i => Task.Run(async () => {
|
||||
await using var scope = sp.CreateAsyncScope();
|
||||
await using var _db = scope.ServiceProvider.GetRequiredService<SpacebarDbContext>();
|
||||
// set timeout
|
||||
_db.Database.SetCommandTimeout(6000);
|
||||
await _db.Database.ExecuteSqlRawAsync($"""
|
||||
DELETE FROM messages
|
||||
WHERE channel_id = '{channelId}'
|
||||
AND guild_id = '{guildId}'
|
||||
AND id LIKE '%{i:00}';
|
||||
""");
|
||||
|
||||
Console.WriteLine($"Truncated messages for {channelId} in {guildId} ending with {i}");
|
||||
})).ToList();
|
||||
await Task.WhenAll(tasks);
|
||||
ss.Release();
|
||||
}
|
||||
|
||||
var tasks = channels.Select(c => TruncateChannelMessages(c.Id, c.GuildId)).ToList();
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<T> AggregateAsyncEnumerablesWithoutOrder<T>(params IEnumerable<IAsyncEnumerable<T>> enumerables) {
|
||||
var enumerators = enumerables.Select(e => e.GetAsyncEnumerator()).ToList();
|
||||
var tasks = enumerators.Select(e => e.MoveNextAsync().AsTask()).ToList();
|
||||
|
||||
try {
|
||||
while (tasks.Count > 0) {
|
||||
var completedTask = await Task.WhenAny(tasks);
|
||||
var completedTaskIndex = tasks.IndexOf(completedTask);
|
||||
|
||||
if (completedTask.IsCanceled) {
|
||||
try {
|
||||
await enumerators[completedTaskIndex].DisposeAsync();
|
||||
}
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
enumerators.RemoveAt(completedTaskIndex);
|
||||
tasks.RemoveAt(completedTaskIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await completedTask) {
|
||||
var enumerator = enumerators[completedTaskIndex];
|
||||
yield return enumerator.Current;
|
||||
tasks[completedTaskIndex] = enumerator.MoveNextAsync().AsTask();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
await enumerators[completedTaskIndex].DisposeAsync();
|
||||
}
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
enumerators.RemoveAt(completedTaskIndex);
|
||||
tasks.RemoveAt(completedTaskIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
foreach (var enumerator in enumerators) {
|
||||
try {
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,22 @@
|
||||
using System.Buffers.Text;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ArcaneLibs.Extensions;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Spacebar.AdminAPI.Extensions;
|
||||
using Spacebar.Db.Contexts;
|
||||
using Spacebar.Db.Models;
|
||||
|
||||
namespace Spacebar.AdminAPI.Middleware;
|
||||
|
||||
public class AuthenticationMiddleware(RequestDelegate next) {
|
||||
public async Task Invoke(HttpContext context) {
|
||||
if(Environment.GetEnvironmentVariable("SB_ADMIN_API_DISABLE_AUTH") == "true") {
|
||||
private static Dictionary<string, User> _userCache = new();
|
||||
private static Dictionary<string, DateTime> _userCacheExpiry = new();
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, IServiceProvider sp) {
|
||||
if (context.Request.Path.StartsWithSegments("/ping")) {
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!context.Request.Headers.ContainsKey("Authorization")) {
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Authorization header is missing");
|
||||
@ -46,8 +46,33 @@ public class AuthenticationMiddleware(RequestDelegate next) {
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(res.ClaimsIdentity.Claims.Select(x => $"{x.Type} : {x.Value}").ToJson());
|
||||
User user;
|
||||
if (_userCacheExpiry.ContainsKey(token) && _userCacheExpiry[token] < DateTime.Now) {
|
||||
_userCache.Remove(token);
|
||||
_userCacheExpiry.Remove(token);
|
||||
}
|
||||
|
||||
if (!_userCache.ContainsKey(token)) {
|
||||
var db = sp.GetRequiredService<SpacebarDbContext>();
|
||||
user = await db.Users.FindAsync(res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value)
|
||||
?? throw new InvalidOperationException();
|
||||
_userCache[token] = user;
|
||||
_userCacheExpiry[token] = DateTime.Now.AddMinutes(5);
|
||||
}
|
||||
|
||||
user = _userCache[token];
|
||||
if (user.Disabled) {
|
||||
context.Response.StatusCode = 403;
|
||||
await context.Response.WriteAsync("User is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.Deleted) {
|
||||
context.Response.StatusCode = 403;
|
||||
await context.Response.WriteAsync("User is deleted");
|
||||
return;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,9 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http.Timeouts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Spacebar.AdminAPI.Middleware;
|
||||
using Spacebar.AdminAPI.Services;
|
||||
using Spacebar.Db.Contexts;
|
||||
using Spacebar.RabbitMqUtilities;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@ -10,10 +12,13 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers(options => {
|
||||
options.MaxValidationDepth = null;
|
||||
options.MaxIAsyncEnumerableBufferLimit = 100;
|
||||
// options.MaxIAsyncEnumerableBufferLimit = 1;
|
||||
}).AddJsonOptions(options => {
|
||||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
options.JsonSerializerOptions.WriteIndented = true;
|
||||
// options.JsonSerializerOptions.DefaultBufferSize = ;
|
||||
}).AddMvcOptions(o=> {
|
||||
o.SuppressOutputFormatterBuffering = true;
|
||||
});
|
||||
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
@ -23,6 +28,10 @@ builder.Services.AddDbContextPool<SpacebarDbContext>(options => {
|
||||
.UseNpgsql(builder.Configuration.GetConnectionString("Spacebar"))
|
||||
.EnableDetailedErrors();
|
||||
});
|
||||
builder.Services.AddScoped<AuthenticationService>();
|
||||
builder.Services.AddScoped<Configuration>();
|
||||
builder.Services.AddSingleton<RabbitMQConfiguration>();
|
||||
builder.Services.AddSingleton<RabbitMQService>();
|
||||
|
||||
builder.Services.AddRequestTimeouts(x => {
|
||||
x.DefaultPolicy = new RequestTimeoutPolicy {
|
||||
@ -43,6 +52,7 @@ builder.Services.AddCors(options => {
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.UsePathBase("/_spacebar/admin");
|
||||
app.UseCors("Open");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using ArcaneLibs.Extensions;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Spacebar.Db.Contexts;
|
||||
using Spacebar.Db.Models;
|
||||
|
||||
namespace Spacebar.AdminAPI.Services;
|
||||
|
||||
public class AuthenticationService(SpacebarDbContext db) {
|
||||
private static Dictionary<string, User> _userCache = new();
|
||||
private static Dictionary<string, DateTime> _userCacheExpiry = new();
|
||||
|
||||
public async Task<User> GetCurrentUser(HttpRequest request) {
|
||||
if (!request.Headers.ContainsKey("Authorization")) {
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var token = request.Headers["Authorization"].ToString().Split(' ').Last();
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var secretFile = File.ReadAllText("../../../jwt.key.pub");
|
||||
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
key.ImportFromPem(secretFile);
|
||||
|
||||
var res = await handler.ValidateTokenAsync(token, new TokenValidationParameters {
|
||||
IssuerSigningKey = new ECDsaSecurityKey(key),
|
||||
ValidAlgorithms = new[] { "ES512" },
|
||||
LogValidationExceptions = true,
|
||||
// These are required to be false for the token to be valid as they aren't provided by the token
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateAudience = false,
|
||||
});
|
||||
|
||||
if (!res.IsValid) {
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
return await db.Users.FindAsync(res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Spacebar.AdminAPI.Services;
|
||||
|
||||
public class Configuration {
|
||||
public Configuration(IConfiguration configuration) {
|
||||
configuration.GetRequiredSection("SpacebarAdminApi").Bind(this);
|
||||
}
|
||||
}
|
||||
@ -10,11 +10,14 @@
|
||||
<PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Spacebar.AdminApi.Models\Spacebar.AdminApi.Models.csproj" />
|
||||
<ProjectReference Include="..\Spacebar.Db\Spacebar.Db.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Spacebar.RabbitMqUtilities\Spacebar.RabbitMqUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
@Spacebar.AdminAPI_HostAddress = http://localhost:5112
|
||||
|
||||
GET {{Spacebar.AdminAPI_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
33
extra/admin-api/Spacebar.AdminApi.Models/ApplicationModel.cs
Normal file
33
extra/admin-api/Spacebar.AdminApi.Models/ApplicationModel.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace Spacebar.AdminApi.Models;
|
||||
|
||||
public class ApplicationModel {
|
||||
public string Id { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public string? Icon { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public bool Hook { get; set; }
|
||||
public bool BotPublic { get; set; }
|
||||
public bool BotRequireCodeGrant { get; set; }
|
||||
public int Flags { get; set; }
|
||||
public string? RedirectUris { get; set; }
|
||||
public int? RpcApplicationState { get; set; }
|
||||
public int? StoreApplicationState { get; set; }
|
||||
public int? VerificationState { get; set; }
|
||||
public string? InteractionsEndpointUrl { get; set; }
|
||||
public bool? IntegrationPublic { get; set; }
|
||||
public bool? IntegrationRequireCodeGrant { get; set; }
|
||||
public int? DiscoverabilityState { get; set; }
|
||||
public int? DiscoveryEligibilityFlags { get; set; }
|
||||
public string? Tags { get; set; }
|
||||
public string? CoverImage { get; set; }
|
||||
public string? InstallParams { get; set; }
|
||||
public string? TermsOfServiceUrl { get; set; }
|
||||
public string? PrivacyPolicyUrl { get; set; }
|
||||
public string? GuildId { get; set; }
|
||||
public string? CustomInstallUrl { get; set; }
|
||||
public string? OwnerId { get; set; }
|
||||
public string? BotUserId { get; set; }
|
||||
public string? TeamId { get; set; }
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Spacebar.AdminApi.Models;
|
||||
|
||||
public class AsyncActionResult {
|
||||
public AsyncActionResult() { }
|
||||
|
||||
public AsyncActionResult(string type, object? data) {
|
||||
MessageType = type;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string MessageType { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public object Data { get; set; }
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
57
extra/admin-api/Spacebar.AdminApi.Models/UserModel.cs
Normal file
57
extra/admin-api/Spacebar.AdminApi.Models/UserModel.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace Spacebar.AdminApi.Models;
|
||||
|
||||
public class UserModel {
|
||||
public string Id { get; set; } = null!;
|
||||
public string Username { get; set; } = null!;
|
||||
public string Discriminator { get; set; } = null!;
|
||||
public string? Avatar { get; set; }
|
||||
public int? AccentColor { get; set; }
|
||||
public string? Banner { get; set; }
|
||||
public string? ThemeColors { get; set; }
|
||||
public string? Pronouns { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public bool Desktop { get; set; }
|
||||
public bool Mobile { get; set; }
|
||||
public bool Premium { get; set; }
|
||||
public int PremiumType { get; set; }
|
||||
public bool Bot { get; set; }
|
||||
public string Bio { get; set; } = null!;
|
||||
public bool System { get; set; }
|
||||
public bool NsfwAllowed { get; set; }
|
||||
public bool MfaEnabled { get; set; }
|
||||
public bool WebauthnEnabled { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? PremiumSince { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public ulong Flags { get; set; }
|
||||
public ulong PublicFlags { get; set; }
|
||||
public ulong Rights { get; set; }
|
||||
public ApplicationModel? ApplicationBotUser { get; set; }
|
||||
public List<ConnectedAccountModel> ConnectedAccounts { get; set; } = new();
|
||||
public int GuildCount { get; set; }
|
||||
public int OwnedGuildCount { get; set; }
|
||||
public int SessionCount { get; set; }
|
||||
public int TemplateCount { get; set; }
|
||||
public int VoiceStateCount { get; set; }
|
||||
public int MessageCount { get; set; }
|
||||
|
||||
public class ConnectedAccountModel {
|
||||
public string Id { get; set; } = null!;
|
||||
public string ExternalId { get; set; } = null!;
|
||||
public string? UserId { get; set; }
|
||||
public bool FriendSync { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool Revoked { get; set; }
|
||||
public int ShowActivity { get; set; }
|
||||
public string Type { get; set; } = null!;
|
||||
public bool Verified { get; set; }
|
||||
public int Visibility { get; set; }
|
||||
public string Integrations { get; set; } = null!;
|
||||
public string? Metadata { get; set; }
|
||||
public int MetadataVisibility { get; set; }
|
||||
public bool TwoWayLink { get; set; }
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,9 @@ using Spacebar.Db.Models;
|
||||
|
||||
namespace Spacebar.Db.Contexts;
|
||||
|
||||
public partial class SpacebarDbContext : DbContext
|
||||
{
|
||||
public SpacebarDbContext(DbContextOptions<SpacebarDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
public partial class SpacebarDbContext(DbContextOptions<SpacebarDbContext> options) : DbContext(options) {
|
||||
public SpacebarDbContext Clone() {
|
||||
return new SpacebarDbContext(options);
|
||||
}
|
||||
|
||||
public virtual DbSet<Application> Applications { get; set; }
|
||||
|
||||
@ -92,11 +92,11 @@ public partial class User
|
||||
[Column("email", TypeName = "character varying")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Column("flags", TypeName = "character varying")]
|
||||
public string Flags { get; set; }
|
||||
[Column("flags")]
|
||||
public ulong Flags { get; set; }
|
||||
|
||||
[Column("public_flags")]
|
||||
public int PublicFlags { get; set; }
|
||||
public ulong PublicFlags { get; set; }
|
||||
|
||||
[Column("purchased_flags")]
|
||||
public int PurchasedFlags { get; set; }
|
||||
@ -105,7 +105,7 @@ public partial class User
|
||||
public int PremiumUsageFlags { get; set; }
|
||||
|
||||
[Column("rights")]
|
||||
public long Rights { get; set; }
|
||||
public ulong Rights { get; set; }
|
||||
|
||||
[Column("data")]
|
||||
public string Data { get; set; } = null!;
|
||||
|
||||
@ -7,6 +7,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Db", "Spacebar.Db\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminAPI", "Spacebar.AdminAPI\Spacebar.AdminAPI.csproj", "{00E58C53-0AC1-4113-8CCF-D299861EA8D3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.RabbitMqUtilities", "Utilities\Spacebar.RabbitMqUtilities\Spacebar.RabbitMqUtilities.csproj", "{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminAPITest", "Utilities\Spacebar.AdminAPITest\Spacebar.AdminAPITest.csproj", "{374314B2-7149-4316-8750-4E1E7BF6C3B4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminAPI.TestClient", "Utilities\Spacebar.AdminAPI.TestClient\Spacebar.AdminAPI.TestClient.csproj", "{498DAD05-336E-4851-ABD8-4E7CCA8312B0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminApi.Models", "Spacebar.AdminApi.Models\Spacebar.AdminApi.Models.csproj", "{5F138C70-EAFA-4C7C-8B90-EFB9624B235C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminApi.PrepareTestData", "Utilities\Spacebar.AdminApi.PrepareTestData\Spacebar.AdminApi.PrepareTestData.csproj", "{BCC6501C-16A7-4787-BA47-52DAE06718A8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -24,5 +36,31 @@ Global
|
||||
{00E58C53-0AC1-4113-8CCF-D299861EA8D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{00E58C53-0AC1-4113-8CCF-D299861EA8D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{00E58C53-0AC1-4113-8CCF-D299861EA8D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{374314B2-7149-4316-8750-4E1E7BF6C3B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{374314B2-7149-4316-8750-4E1E7BF6C3B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{374314B2-7149-4316-8750-4E1E7BF6C3B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{374314B2-7149-4316-8750-4E1E7BF6C3B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{498DAD05-336E-4851-ABD8-4E7CCA8312B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{498DAD05-336E-4851-ABD8-4E7CCA8312B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{498DAD05-336E-4851-ABD8-4E7CCA8312B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{498DAD05-336E-4851-ABD8-4E7CCA8312B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F138C70-EAFA-4C7C-8B90-EFB9624B235C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F138C70-EAFA-4C7C-8B90-EFB9624B235C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F138C70-EAFA-4C7C-8B90-EFB9624B235C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F138C70-EAFA-4C7C-8B90-EFB9624B235C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BCC6501C-16A7-4787-BA47-52DAE06718A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BCC6501C-16A7-4787-BA47-52DAE06718A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BCC6501C-16A7-4787-BA47-52DAE06718A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BCC6501C-16A7-4787-BA47-52DAE06718A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}
|
||||
{374314B2-7149-4316-8750-4E1E7BF6C3B4} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}
|
||||
{498DAD05-336E-4851-ABD8-4E7CCA8312B0} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}
|
||||
{BCC6501C-16A7-4787-BA47-52DAE06718A8} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@ -0,0 +1,16 @@
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #333333;
|
||||
border-bottom: 1px solid #444444;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">Spacebar.AdminAPI.TestClient</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Users">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Users
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Guilds">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Guilds
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu() {
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
@page "/"
|
||||
@using System.Net.Http.Headers
|
||||
@using Spacebar.AdminAPI.TestClient.Services
|
||||
@inject Config Config
|
||||
@inject ILocalStorageService LocalStorage
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<span style="@($"color: {(IsApiUrlValid ? "green" : "red")};")">Spacebar API URL: </span>
|
||||
<InputText @bind-Value:get="Config.ApiUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.ApiUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<!-- <span style="@($"color: {(IsGatewayUrlValid ? "green" : "red")};")">Spacebar Gateway URL: </span> -->
|
||||
<!-- <InputText @bind-Value="GatewayUrl" /> -->
|
||||
<!-- <br /> -->
|
||||
|
||||
<span style="@($"color: {(IsCdnUrlValid ? "green" : "red")};")">Spacebar CDN URL: </span>
|
||||
<InputText @bind-Value:get="Config.CdnUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.CdnUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<span style="@($"color: {(IsAdminApiUrlValid ? "green" : "red")};")">Spacebar Admin API URL: </span>
|
||||
<InputText @bind-Value:get="Config.AdminUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.AdminUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<span style="@($"color: {(IsAccessTokenValid ? "green" : "red")};")">Access Token: </span>
|
||||
<InputText @bind-Value:get="Config.AccessToken" @bind-Value:set="@(async (v) => {
|
||||
Config.AccessToken = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<a href="/login">New access token</a>
|
||||
<br/>
|
||||
|
||||
@code {
|
||||
|
||||
private bool IsApiUrlValid { get; set; }
|
||||
|
||||
// private bool IsGatewayUrlValid { get; set; }
|
||||
private bool IsCdnUrlValid { get; set; }
|
||||
private bool IsAdminApiUrlValid { get; set; }
|
||||
private bool IsAccessTokenValid { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await ValidateAndSaveConfig();
|
||||
}
|
||||
|
||||
private async Task ValidateAndSaveConfig() {
|
||||
await LocalStorage.SetItemAsync("sb_admin_tc_config", Config);
|
||||
|
||||
using var hc = new HttpClient();
|
||||
HttpResponseMessage response;
|
||||
try {
|
||||
response = await hc.GetAsync(Config.ApiUrl + "/api/v9/policies/instance/domains");
|
||||
IsApiUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsApiUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
// response = await hc.GetAsync(Config.GatewayUrl + "/api/v9/policies/instance");
|
||||
// IsGatewayUrlValid = response.IsSuccessStatusCode;
|
||||
// StateHasChanged();
|
||||
|
||||
try {
|
||||
response = await hc.GetAsync(Config.CdnUrl + "/ping");
|
||||
IsCdnUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsCdnUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
try {
|
||||
response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/ping");
|
||||
IsAdminApiUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsAdminApiUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
try {
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Config.AdminUrl + "/_spacebar/admin/whoami");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
response = await hc.SendAsync(request);
|
||||
IsAccessTokenValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsAccessTokenValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
@page "/Login"
|
||||
@using System.Text.Json.Nodes
|
||||
@using Spacebar.AdminAPI.TestClient.Services
|
||||
@inject ILocalStorageService LocalStorage
|
||||
@inject Config Config
|
||||
@inject NavigationManager Navigation
|
||||
<h3>Login</h3>
|
||||
|
||||
<span>Email: </span>
|
||||
<InputText @bind-Value="Email"/>
|
||||
<br/>
|
||||
|
||||
<span>Password: </span>
|
||||
<InputText type="password" @bind-Value="Password"/>
|
||||
<br/>
|
||||
|
||||
<button @onclick="DoLogin">Login</button>
|
||||
<br/>
|
||||
|
||||
<pre style="color: red; font-family: 'JetBrains Mono',monospace">@Error</pre>
|
||||
|
||||
|
||||
@code {
|
||||
private string Email { get; set; }
|
||||
private string Password { get; set; }
|
||||
private string Error { get; set; }
|
||||
|
||||
private async Task DoLogin() {
|
||||
HttpResponseMessage response;
|
||||
using var hc = new HttpClient();
|
||||
|
||||
try {
|
||||
response = await hc.PostAsJsonAsync(Config.ApiUrl + "/api/v9/auth/login", new {
|
||||
login = Email,
|
||||
password = Password,
|
||||
login_source = "Spacebar Admin API Test Client",
|
||||
undelete = false
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
Error = e.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode) {
|
||||
Error = await response.Content.ReadAsStringAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadFromJsonAsync<JsonObject>();
|
||||
var accessToken = content!["token"].ToString();
|
||||
Config.AccessToken = accessToken;
|
||||
await LocalStorage.SetItemAsync("sb_admin_tc_config", Config);
|
||||
Navigation.NavigateTo("/", true, true);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
@page "/Users"
|
||||
@using System.Net.Http.Headers
|
||||
@using System.Reflection
|
||||
@using Spacebar.AdminApi.Models
|
||||
@using Spacebar.AdminAPI.TestClient.Services
|
||||
@using ArcaneLibs.Blazor.Components
|
||||
@inject Config Config
|
||||
@inject ILocalStorageService LocalStorage
|
||||
|
||||
<PageTitle>Users</PageTitle>
|
||||
|
||||
<details>
|
||||
<summary>Displayed columns</summary>
|
||||
@foreach (var column in DisplayedColumns) {
|
||||
var value = column.Value;
|
||||
<span>
|
||||
<InputCheckbox @bind-Value:get="@(value)" @bind-Value:set="@(b => {
|
||||
DisplayedColumns[column.Key] = b;
|
||||
StateHasChanged();
|
||||
})"/>
|
||||
@column.Key.Name
|
||||
</span>
|
||||
<br/>
|
||||
}
|
||||
</details>
|
||||
|
||||
<table class="table table-bordered">
|
||||
@{
|
||||
var columns = DisplayedColumns.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
|
||||
}
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach (var column in columns) {
|
||||
<th>@column.Name</th>
|
||||
}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in UserList) {
|
||||
<tr>
|
||||
@foreach (var column in columns) {
|
||||
<td>@column.GetValue(user)</td>
|
||||
}
|
||||
<td>
|
||||
<LinkButton href="@($"/Users/Delete/{user.Id}")" Color="#ff0000">Delete</LinkButton>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private Dictionary<PropertyInfo, bool> DisplayedColumns { get; set; } = typeof(UserModel).GetProperties()
|
||||
.ToDictionary(p => p, p => p.Name == "Username" || p.Name == "Id" || p.Name == "MessageCount");
|
||||
|
||||
private List<UserModel> UserList { get; set; } = new();
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
using var hc = new HttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
var response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/users/");
|
||||
if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
var content = response.Content.ReadFromJsonAsAsyncEnumerable<UserModel>();
|
||||
await foreach (var user in content) {
|
||||
UserList.Add(user);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
@page "/Users/Delete/{Id}"
|
||||
@using System.Net.Http.Headers
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Nodes
|
||||
@using ArcaneLibs.Extensions
|
||||
@using Spacebar.AdminApi.Models
|
||||
@using Spacebar.AdminAPI.TestClient.Services
|
||||
@inject Config Config
|
||||
<h3>UsersDelete - @Id</h3>
|
||||
|
||||
Deleted @ChannelDeleteProgress.Sum(x=>x.Value.Deleted) messages so far!
|
||||
@foreach (var (channel, progress) in ChannelDeleteProgress.Where(x=>x.Value.Deleted != x.Value.Total).OrderByDescending(x=>x.Value.Progress)) {
|
||||
<div>@channel: @progress.Total total, @progress.Deleted deleted</div>
|
||||
<progress max="@progress.Total" value="@progress.Deleted"></progress>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public required string Id { get; set; }
|
||||
|
||||
private Dictionary<string, DeleteProgress> ChannelDeleteProgress { get; set; } = new();
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
using var hc = new HttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
var response = await hc.GetAsync(Config.AdminUrl + $"/_spacebar/admin/Users/{Id}/delete?messageDeleteChunkSize=100");
|
||||
if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
var content = response.Content.ReadFromJsonAsAsyncEnumerable<AsyncActionResult>();
|
||||
await foreach (var actionResult in content) {
|
||||
Console.WriteLine(actionResult.ToJson(indent: false));
|
||||
switch (actionResult.MessageType) {
|
||||
case "STATS": {
|
||||
var data = JsonSerializer.Deserialize<JsonObject>(actionResult.Data.ToJson());
|
||||
ChannelDeleteProgress = data!["messages_per_channel"]!
|
||||
.Deserialize<Dictionary<string, int>>()!
|
||||
.ToDictionary(x=>x.Key, x=>new DeleteProgress { Total = x.Value });
|
||||
break;
|
||||
}
|
||||
case "BULK_DELETED": {
|
||||
var data = JsonSerializer.Deserialize<JsonObject>(actionResult.Data.ToJson());
|
||||
ChannelDeleteProgress[data!["channel_id"]!.ToString()].Deleted += data!["deleted"]!.GetValue<int>();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
Console.WriteLine($"Unknown message type: {actionResult.MessageType}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteProgress {
|
||||
public int Total { get; set; }
|
||||
public int Deleted { get; set; } = 0;
|
||||
public float Progress => (float)Deleted / Total;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Spacebar.AdminAPI.TestClient;
|
||||
using Spacebar.AdminAPI.TestClient.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
try {
|
||||
builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.json"));
|
||||
#if DEBUG
|
||||
builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.Development.json"));
|
||||
#endif
|
||||
}
|
||||
catch (HttpRequestException e) {
|
||||
if (e.StatusCode == HttpStatusCode.NotFound)
|
||||
Console.WriteLine("Could not load appsettings, server returned 404.");
|
||||
else
|
||||
Console.WriteLine("Could not load appsettings: " + e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine("Could not load appsettings: " + e);
|
||||
}
|
||||
|
||||
builder.Logging.AddConfiguration(
|
||||
builder.Configuration.GetSection("Logging"));
|
||||
|
||||
builder.Services.AddBlazoredLocalStorageAsSingleton(config => {
|
||||
config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
|
||||
config.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
config.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
|
||||
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
|
||||
config.JsonSerializerOptions.WriteIndented = false;
|
||||
});
|
||||
|
||||
// temporarily build the service provider to read config
|
||||
{
|
||||
await using var sp = builder.Services.BuildServiceProvider();
|
||||
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
||||
var config = await localStorage.GetItemAsync<Config>("sb_admin_tc_config");
|
||||
if (config == null) {
|
||||
config = new Config();
|
||||
await localStorage.SetItemAsync("sb_admin_tc_config", config);
|
||||
}
|
||||
builder.Services.AddSingleton(config);
|
||||
}
|
||||
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5179",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Spacebar.AdminAPI.TestClient.Services;
|
||||
|
||||
public class Config {
|
||||
[JsonPropertyName("api_url")]
|
||||
public string ApiUrl { get; set; } = "http://localhost:3001";
|
||||
|
||||
[JsonPropertyName("gateway_url")]
|
||||
public string GatewayUrl { get; set; } = "http://localhost:3002";
|
||||
|
||||
[JsonPropertyName("cdn_url")]
|
||||
public string CdnUrl { get; set; } = "http://localhost:3003";
|
||||
|
||||
[JsonPropertyName("admin_url")]
|
||||
public string AdminUrl { get; set; } = "http://localhost:5112";
|
||||
|
||||
[JsonPropertyName("access_token")]
|
||||
public string? AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
|
||||
<UseBlazorWebAssembly>true</UseBlazorWebAssembly>
|
||||
<BlazorEnableCompression>false</BlazorEnableCompression>
|
||||
<BlazorCacheBootResources>false</BlazorCacheBootResources>
|
||||
<!-- <RunAOTCompilation>true</RunAOTCompilation>-->
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Spacebar.AdminApi.Models\Spacebar.AdminApi.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Blazored.LocalStorage
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Spacebar.AdminAPI.TestClient
|
||||
@using Spacebar.AdminAPI.TestClient.Layout
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Spacebar Admin API Test client</title>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="lib/jetbrains-mono/jetbrains-mono.css"/>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link href="Spacebar.AdminAPI.TestClient.styles.css" rel="stylesheet"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,118 @@
|
||||
/* source: https://gist.github.com/aasmpro/95776294ecf48bd7d0562504bad848ea */
|
||||
|
||||
/* normal fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url("./ttf/JetBrainsMono-Thin.ttf") format("truetype");
|
||||
src: url("./webfonts/JetBrainsMono-Thin.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-Light.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* italic fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url("./webfonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-LightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs
Normal file
12
extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs
Normal file
@ -0,0 +1,12 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using ArcaneLibs.Extensions;
|
||||
|
||||
Console.WriteLine("Hello, World!");
|
||||
using var hc = new HttpClient();
|
||||
var response = hc.GetFromJsonAsAsyncEnumerable<object>("http://localhost:5112/users/1183568750931099679/deactivate");
|
||||
await foreach (var item in response) {
|
||||
Console.WriteLine(item.ToJson(indent: false));
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="ArcaneLibs">
|
||||
<HintPath>..\..\..\..\..\..\..\.nuget\packages\arcanelibs\1.0.0-preview.20241210-161342\lib\net9.0\ArcaneLibs.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,24 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using ArcaneLibs;
|
||||
using Spacebar.AdminApi.PrepareTestData.TestDataTypes;
|
||||
|
||||
Console.WriteLine("Hello, World!");
|
||||
var tests = ClassCollector<ITestData>.ResolveFromAllAccessibleAssemblies();
|
||||
foreach (var test in tests) {
|
||||
Console.WriteLine(test.Name);
|
||||
}
|
||||
|
||||
Console.Write("Enter test type to run: ");
|
||||
var testType = Console.ReadLine();
|
||||
var testToRun = tests.FirstOrDefault(t => t.Name == testType);
|
||||
var runMethod = testToRun?.GetMethod("Run");
|
||||
|
||||
if (runMethod != null) {
|
||||
Console.WriteLine($"Running test {testToRun.FullName}...");
|
||||
var task = runMethod.Invoke(testToRun, null) as Task;
|
||||
await task!;
|
||||
Console.WriteLine($"Test {testToRun.FullName} completed.");
|
||||
} else {
|
||||
Console.WriteLine("Test not found.");
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20241210-161342" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,5 @@
|
||||
namespace Spacebar.AdminApi.PrepareTestData.TestDataTypes;
|
||||
|
||||
public interface ITestData {
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Spacebar.AdminApi.PrepareTestData.TestDataTypes;
|
||||
|
||||
public class MessageSpamTestData : ITestData {
|
||||
public static async Task Run() {
|
||||
using var hc = new HttpClient();
|
||||
var token = await Utils.CreateUser();
|
||||
hc.DefaultRequestHeaders.Authorization = new("Bearer", token);
|
||||
|
||||
int guildCount = 1;
|
||||
int channelCount = 100; // per guild
|
||||
int messageCount = 1000; // per channel
|
||||
|
||||
for (int guild = 0; guild < guildCount; guild++) {
|
||||
var guildId = await Utils.CreateGuild(token);
|
||||
// for (int channel = 0; channel < channelCount; channel++) {
|
||||
// Console.WriteLine($"> Creating channel {channel} in guild {guild}...");
|
||||
// var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new {
|
||||
// name = Guid.NewGuid().ToString()[0..30],
|
||||
// type = 0
|
||||
// });
|
||||
// var channelResponse = await channelRequest.Content.ReadFromJsonAsync<JsonObject>();
|
||||
// var channelId = channelResponse!["id"]!.ToString();
|
||||
// await SendMessages(hc, channelId, messageCount);
|
||||
// }
|
||||
|
||||
var ss = new SemaphoreSlim(16,16);
|
||||
var tasks = Enumerable.Range(0, channelCount).Select(async channel => {
|
||||
await ss.WaitAsync();
|
||||
Console.WriteLine($"> Creating channel {channel} in guild {guildId}...");
|
||||
var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new {
|
||||
name = Guid.NewGuid().ToString()[0..30],
|
||||
type = 0
|
||||
});
|
||||
var channelResponse = await channelRequest.Content.ReadFromJsonAsync<JsonObject>();
|
||||
var channelId = channelResponse!["id"]!.ToString();
|
||||
await SendMessages(hc, channelId, messageCount);
|
||||
ss.Release();
|
||||
});
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreateChannels(HttpClient hc, string guildId, int channelCount) {
|
||||
var tasks = Enumerable.Range(0, channelCount).Select(async channel => {
|
||||
Console.WriteLine($"> Creating channel {channel} in guild {guildId}...");
|
||||
var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new {
|
||||
name = Guid.NewGuid().ToString()[0..30],
|
||||
type = 0
|
||||
});
|
||||
var channelResponse = await channelRequest.Content.ReadFromJsonAsync<JsonObject>();
|
||||
});
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private static async Task SendMessages(HttpClient hc, string channelId, int maxMessageCount) {
|
||||
// var ss = new SemaphoreSlim(32, 32);
|
||||
// var tasks = Enumerable.Range(0, Random.Shared.Next((int)(0.75 * maxMessageCount), maxMessageCount)).Select(async message => {
|
||||
// var success = false;
|
||||
// while (!success) {
|
||||
// await ss.WaitAsync();
|
||||
// Console.WriteLine($"> Sending message {message} in channel {channelId}...");
|
||||
// var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new {
|
||||
// content = Guid.NewGuid().ToString()
|
||||
// });
|
||||
// var messageResponse = await messageReq.Content.ReadFromJsonAsync<JsonObject>();
|
||||
// if (messageResponse.ContainsKey("id")) {
|
||||
// success = true;
|
||||
// Console.WriteLine(messageResponse!["id"]!.ToString());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ss.Release();
|
||||
// });
|
||||
// await Task.WhenAll(tasks);
|
||||
|
||||
var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new {
|
||||
content = Guid.NewGuid().ToString()
|
||||
});
|
||||
var messageResponse = await messageReq.Content.ReadFromJsonAsync<JsonObject>();
|
||||
if (messageResponse.ContainsKey("id")) {
|
||||
await hc.GetAsync($"http://localhost:5112/Users/duplicate/{messageResponse!["id"]!.ToString()}?count={maxMessageCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ArcaneLibs.Extensions;
|
||||
|
||||
namespace Spacebar.AdminApi.PrepareTestData;
|
||||
|
||||
public static class Utils {
|
||||
public static async Task<string> CreateUser() {
|
||||
Console.WriteLine("> Creating user...");
|
||||
using var hc = new HttpClient();
|
||||
var registerRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/auth/register", new {
|
||||
username = Guid.NewGuid().ToString()[0..30],
|
||||
password = "password",
|
||||
email = $"{Guid.NewGuid()}@example.com",
|
||||
consent = true,
|
||||
date_of_birth = "2000-01-01",
|
||||
});
|
||||
var registerResponse = await registerRequest.Content.ReadFromJsonAsync<JsonObject>();
|
||||
return registerResponse!["token"]!.ToString();
|
||||
}
|
||||
|
||||
public static async Task<string> CreateGuild(string token) {
|
||||
using var hc = new HttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new("Bearer", token);
|
||||
|
||||
Console.WriteLine("> Creating guild...");
|
||||
var guildRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/guilds", new {
|
||||
name = Guid.NewGuid().ToString()[0..30]
|
||||
});
|
||||
|
||||
var guildResponse = await guildRequest.Content.ReadFromJsonAsync<JsonObject>();
|
||||
var guildId = guildResponse!["id"]!.ToString();
|
||||
return guildId;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Spacebar.RabbitMqUtilities;
|
||||
|
||||
public class RabbitMQConfiguration {
|
||||
public RabbitMQConfiguration(IConfiguration configuration) {
|
||||
configuration.GetRequiredSection("RabbitMQ").Bind(this);
|
||||
}
|
||||
public required string Host { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required string Password { get; set; }
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace Spacebar.RabbitMqUtilities;
|
||||
|
||||
public interface IRabbitMQService {
|
||||
IConnection CreateChannel();
|
||||
}
|
||||
|
||||
public class RabbitMQService(RabbitMQConfiguration config) : IRabbitMQService {
|
||||
public IConnection CreateChannel() {
|
||||
var connection = new ConnectionFactory {
|
||||
UserName = config.Username,
|
||||
Password = config.Password,
|
||||
HostName = config.Host,
|
||||
// DispatchConsumersAsync = true
|
||||
};
|
||||
|
||||
var channel = connection.CreateConnection();
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1 +0,0 @@
|
||||
/nix/store/0q9yki1d9czy7i7mly8gy3ffjvc3hkqv-postgresql-16.5-man
|
||||
@ -36,5 +36,9 @@ export function CORS(req: Request, res: Response, next: NextFunction) {
|
||||
req.header("Access-Control-Request-Methods") || "*",
|
||||
);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@ -64,10 +64,17 @@ import { PreloadedUserSettings } from "discord-protos";
|
||||
// TODO: user sharding
|
||||
// TODO: check privileged intents, if defined in the config
|
||||
|
||||
function logAuth(message: string) {
|
||||
if (process.env.LOG_AUTH != "true") return;
|
||||
console.log(`[Gateway/Auth] ${message}`);
|
||||
}
|
||||
|
||||
const tryGetUserFromToken = async (...args: Parameters<typeof checkToken>) => {
|
||||
logAuth("Checking token");
|
||||
try {
|
||||
return (await checkToken(...args)).user;
|
||||
} catch (e) {
|
||||
console.log("[Gateway] Error when identifying: ", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -96,7 +103,10 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
||||
relations: ["relationships", "relationships.to", "settings"],
|
||||
select: [...PrivateUserProjection, "relationships"],
|
||||
});
|
||||
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
||||
if (!user) {
|
||||
console.log("[Gateway] Failed to identify user");
|
||||
return this.close(CLOSECODES.Authentication_failed);
|
||||
}
|
||||
this.user_id = user.id;
|
||||
const userQueryTime = taskSw.getElapsedAndReset();
|
||||
|
||||
|
||||
@ -36,6 +36,16 @@ export type UserTokenData = {
|
||||
decoded: { id: string; iat: number };
|
||||
};
|
||||
|
||||
function logAuth(text: string) {
|
||||
if(process.env.LOG_AUTH !== "true") return;
|
||||
console.log(`[AUTH] ${text}`);
|
||||
}
|
||||
|
||||
function rejectAndLog(rejectFunction: (reason?: any) => void, reason: any) {
|
||||
console.error(reason);
|
||||
rejectFunction(reason);
|
||||
}
|
||||
|
||||
export const checkToken = (
|
||||
token: string,
|
||||
opts?: {
|
||||
@ -49,12 +59,16 @@ export const checkToken = (
|
||||
|
||||
const validateUser: jwt.VerifyCallback = async (err, out) => {
|
||||
const decoded = out as UserTokenData["decoded"];
|
||||
if (err || !decoded) return reject("Invalid Token meow " + err);
|
||||
if (err || !decoded) {
|
||||
logAuth("validateUser rejected: " + err);
|
||||
return rejectAndLog(reject, "Invalid Token meow " + err);
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { id: decoded.id },
|
||||
select: [
|
||||
...(opts?.select || []),
|
||||
"id",
|
||||
"bot",
|
||||
"disabled",
|
||||
"deleted",
|
||||
@ -64,23 +78,36 @@ export const checkToken = (
|
||||
relations: opts?.relations,
|
||||
});
|
||||
|
||||
if (!user) return reject("User not found");
|
||||
if (!user) {
|
||||
logAuth("validateUser rejected: User not found");
|
||||
return rejectAndLog(reject, "User not found");
|
||||
}
|
||||
|
||||
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
|
||||
if (
|
||||
decoded.iat * 1000 <
|
||||
new Date(user.data.valid_tokens_since).setSeconds(0, 0)
|
||||
)
|
||||
return reject("Invalid Token");
|
||||
) {
|
||||
logAuth("validateUser rejected: Token not yet valid");
|
||||
return rejectAndLog(reject, "Invalid Token");
|
||||
}
|
||||
|
||||
if (user.disabled) return reject("User disabled");
|
||||
if (user.deleted) return reject("User not found");
|
||||
if (user.disabled) {
|
||||
logAuth("validateUser rejected: User disabled");
|
||||
return rejectAndLog(reject, "User disabled");
|
||||
}
|
||||
if (user.deleted) {
|
||||
logAuth("validateUser rejected: User deleted");
|
||||
return rejectAndLog(reject, "User not found");
|
||||
}
|
||||
|
||||
logAuth("validateUser success: " + JSON.stringify({ decoded, user }));
|
||||
return resolve({ decoded, user });
|
||||
};
|
||||
|
||||
const dec = jwt.decode(token, { complete: true });
|
||||
if (!dec) return reject("Could not parse token");
|
||||
logAuth("Decoded token: " + JSON.stringify(dec));
|
||||
|
||||
if (dec.header.alg == "HS256") {
|
||||
jwt.verify(
|
||||
|
||||
Reference in New Issue
Block a user