From b76303c45f122cc0731e92dd4a0020d6f50a0902 Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Fri, 5 Dec 2025 01:17:26 +0530 Subject: [PATCH 01/34] Refactor: Replace getIpAddress with req.ip --- assets/openapi.json | Bin 856601 -> 856601 bytes assets/schemas.json | Bin 380213 -> 380213 bytes src/api/middlewares/RateLimit.ts | 48 ++++---------------- src/api/routes/auth/forgot.ts | 15 ++---- src/api/routes/auth/location-metadata.ts | 4 +- src/api/routes/auth/login.ts | 45 ++++-------------- src/api/routes/auth/register.ts | 8 ++-- src/api/routes/auth/verify/index.ts | 12 ++--- src/api/routes/guilds/#guild_id/bans.ts | 4 +- src/api/routes/guilds/#guild_id/bulk-ban.ts | 4 +- src/api/routes/guilds/#guild_id/regions.ts | 9 +--- src/api/routes/invites/index.ts | 4 +- src/api/routes/voice/regions.ts | 4 +- src/api/util/utility/ipAddress.ts | 33 ++------------ 14 files changed, 48 insertions(+), 142 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index 5d73449d2ca0367103f39f941831459302b60123..39ca2eec6aff21e7f664221a050fb737fd007e9a 100644 GIT binary patch delta 310 zcmbQa#AN0YlMPNR)A#RZ7HXc!x_u@qUliE(|`Lg%1&<3keIALg=>1&6GnN6oajRy(dqNdu*tQ{Jz?A~_k>C1+4O`? zX12{9EP;&eA*Yyum}Ps&DOR;jjONoBw{XZ$e{h3|bNYv;OqyWUWCsiT>94EUjKI9@ z{|g~kET$Lu vut-lgum#%CJ(W#>+1$W*GNY+N`{ikDK+F!r96-zo#9Tnkz5Vhu9=B=$V$yYE delta 302 zcmbQa#AN0YlMPNR+y93z*ivD+Iqv;JV6UckmGvAzEeqX+AB``wPsx?NO(gftY1`)G1blP0VJN7SkD@G3iZCFx8sA!J5fx z`UD*&*2xJ)M$;9-Sva@**RpL8XR diff --git a/assets/schemas.json b/assets/schemas.json index 3e597f0945a1d9b9f941f27b3b0e4bfdb62181c6..4fe2ea4019bececb2da9195bb8cca057f527bce9 100644 GIT binary patch delta 296 zcmdlwOML4ramMQDiq6au&Do6Gvl$tst0ph_BC)-pnDH3nbb)MEkLd?aGP6&vR}-83 zVUpf-19e8$=?1%*#HI%*uWRqn%2x@Hkl_ud-{Z@jGWVR zD_K>iU+7>FoScv$I(K(%k;Jnw3YG$=qN%V?S#X$RH3S y1fptz6O-oj4YwGDrw3RwDhb8MJCx=XC+8O=Cgr3SJAl**HnK==ubIHQ(*^*i*kOVI delta 275 zcmdlwOML4ramMQHx2>477#k(GN-|1UP4Craa^J4d$ymrVy8&_m2-M-C9CRmgI-qi z=?X!NqSGe`GAT?B(A3%frJ9vnn90&?I%7X;6UYD%BLuF96~wZheqkN6l2ClSLup=d Za(+Q#Qch~I1IV(%$&%Y^Ca~_b0RYq. */ -import { getIpAdress } from "@spacebar/api"; import { Config, getRights, listenEvent } from "@spacebar/util"; import { NextFunction, Request, Response, Router } from "express"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; @@ -65,21 +64,14 @@ export default function rateLimit(opts: { if (rights.has("BYPASS_RATE_LIMITS")) return next(); } - const bucket_id = - opts.bucket || - req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); - let executor_id = getIpAdress(req); + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); + let executor_id = req.ip || "127.0.0.1"; if (!opts.onlyIp && req.user_id) executor_id = req.user_id; let max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; - if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) - max_hits = opts.GET; - else if ( - opts.MODIFY && - ["POST", "DELETE", "PATCH", "PUT"].includes(req.method) - ) - max_hits = opts.MODIFY; + if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; + else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; const offender = Cache.get(executor_id + bucket_id); @@ -104,18 +96,13 @@ export default function rateLimit(opts: { } res.set("X-RateLimit-Reset", `${reset}`); - res.set( - "X-RateLimit-Reset-After", - `${Math.max(0, Math.ceil(resetAfterSec))}`, - ); + res.set("X-RateLimit-Reset-After", `${Math.max(0, Math.ceil(resetAfterSec))}`); if (offender.blocked) { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further reset += opts.window * 1000; - offender.expires_at = new Date( - offender.expires_at.getTime() + opts.window * 1000, - ); + offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); @@ -129,10 +116,7 @@ export default function rateLimit(opts: { res .status(429) .set("X-RateLimit-Remaining", "0") - .set( - "Retry-After", - `${Math.max(0, Math.ceil(resetAfterSec))}`, - ) + .set("Retry-After", `${Math.max(0, Math.ceil(resetAfterSec))}`) // TODO: error rate limit message translation .send({ message: "You are being rate limited.", @@ -156,11 +140,7 @@ export default function rateLimit(opts: { // check if error and increment error rate limit if (res.statusCode >= 400 && opts.error) { return hitRoute(hitRouteOpts); - } else if ( - res.statusCode >= 200 && - res.statusCode < 300 && - opts.success - ) { + } else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) { return hitRoute(hitRouteOpts); } }); @@ -213,18 +193,10 @@ export async function initRateLimits(app: Router) { app.use("/webhooks/:webhook_id", rateLimit(routes.webhook)); app.use("/channels/:channel_id", rateLimit(routes.channel)); app.use("/auth/login", rateLimit(routes.auth.login)); - app.use( - "/auth/register", - rateLimit({ onlyIp: true, success: true, ...routes.auth.register }), - ); + app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { - executor_id: string; - bucket_id: string; - max_hits: number; - window: number; -}) { +async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { const id = opts.executor_id + opts.bucket_id; let limit = Cache.get(id); if (!limit) { diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts index 12bc9cb07..6e613820f 100644 --- a/src/api/routes/auth/forgot.ts +++ b/src/api/routes/auth/forgot.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; +import { route, verifyCaptcha } from "@spacebar/api"; import { Config, Email, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { ForgotPasswordSchema } from "@spacebar/schemas" +import { ForgotPasswordSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.post( @@ -38,10 +38,7 @@ router.post( const config = Config.get(); - if ( - config.passwordReset.requireCaptcha && - config.security.captcha.enabled - ) { + if (config.passwordReset.requireCaptcha && config.security.captcha.enabled) { const { sitekey, service } = config.security.captcha; if (!captcha_key) { return res.status(400).json({ @@ -51,7 +48,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ @@ -71,9 +68,7 @@ router.post( if (user && user.email) { Email.sendResetPassword(user, user.email).catch((e) => { - console.error( - `Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`, - ); + console.error(`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`); }); } }, diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index a08c98abe..92f47909d 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { IpDataClient } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); @@ -33,7 +33,7 @@ router.get( async (req: Request, res: Response) => { //TODO //Note: It's most likely related to legal. At the moment Discord hasn't finished this too - const country_code = (await IpDataClient.getIpInfo(getIpAdress(req)))?.country_code; + const country_code = (await IpDataClient.getIpInfo(req.ip!))?.country_code; res.json({ consent_required: false, country_code: country_code, diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 06e3fe43e..c8ef62731 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -16,19 +16,12 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; -import { - Config, - FieldErrors, - User, - WebAuthn, - generateToken, - generateWebAuthnTicket, -} from "@spacebar/util"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { Config, FieldErrors, User, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util"; import bcrypt from "bcrypt"; import crypto from "crypto"; import { Request, Response, Router } from "express"; -import { LoginSchema } from "@spacebar/schemas" +import { LoginSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); export default router; @@ -61,7 +54,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ @@ -74,17 +67,7 @@ router.post( const user = await User.findOneOrFail({ where: [{ phone: login }, { email: login }], - select: [ - "data", - "id", - "disabled", - "deleted", - "totp_secret", - "mfa_enabled", - "webauthn_enabled", - "security_keys", - "verified", - ], + select: ["data", "id", "disabled", "deleted", "totp_secret", "mfa_enabled", "webauthn_enabled", "security_keys", "verified"], relations: ["security_keys", "settings"], }).catch(() => { throw FieldErrors({ @@ -100,10 +83,7 @@ router.post( }); // the salt is saved in the password refer to bcrypt docs - const same_password = await bcrypt.compare( - password, - user.data.hash || "", - ); + const same_password = await bcrypt.compare(password, user.data.hash || ""); if (!same_password) { throw FieldErrors({ login: { @@ -122,8 +102,7 @@ router.post( throw FieldErrors({ login: { code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL", - message: - "Email verification is required, please check your email.", + message: "Email verification is required, please check your email.", }, }); } @@ -152,9 +131,7 @@ router.post( const challenge = JSON.stringify({ publicKey: { ...options, - challenge: Buffer.from(options.challenge).toString( - "base64", - ), + challenge: Buffer.from(options.challenge).toString("base64"), allowCredentials: user.security_keys.map((x) => ({ id: x.key_id, type: "public-key", @@ -178,10 +155,8 @@ router.post( if (undelete) { // undelete refers to un'disable' here - if (user.disabled) - await User.update({ id: user.id }, { disabled: false }); - if (user.deleted) - await User.update({ id: user.id }, { deleted: false }); + if (user.disabled) await User.update({ id: user.id }, { disabled: false }); + if (user.deleted) await User.update({ id: user.id }, { deleted: false }); } else { if (user.deleted) return res.status(400).json({ diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 418f0a529..d4b061be2 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; +import { route, verifyCaptcha } from "@spacebar/api"; import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; @@ -38,7 +38,7 @@ router.post( async (req: Request, res: Response) => { const body = req.body as RegisterSchema; const { register, security, limits } = Config.get(); - const ip = getIpAdress(req); + const ip = req.ip!; // Reg tokens // They're a one time use token that bypasses registration limits ( rates, disabled reg, etc ) @@ -143,7 +143,7 @@ router.post( const ipData = await IpDataClient.getIpInfo(ip); if (ipData) { - if(!ipData.threat) { + if (!ipData.threat) { console.log("Invalid IPData.co response, missing threat field", ipData); } const categories = Object.entries(ipData.threat) @@ -287,7 +287,7 @@ router.post( }, })) >= limits.absoluteRate.register.limit ) { - console.log(`Global register ratelimit exceeded for ${getIpAdress(req)}, ${req.body.username}, ${req.body.invite || "No invite given"}`); + console.log(`Global register ratelimit exceeded for ${req.ip}, ${req.body.username}, ${req.body.invite || "No invite given"}`); throw FieldErrors({ email: { code: "TOO_MANY_REGISTRATIONS", diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index b85120d84..d8e343cd2 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -16,14 +16,8 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; -import { - checkToken, - Config, - FieldErrors, - generateToken, - User, -} from "@spacebar/util"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { checkToken, Config, FieldErrors, generateToken, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); @@ -67,7 +61,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index e253bef9f..973f212dd 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -215,7 +215,7 @@ router.put( const ban = Ban.create({ user_id: banned_user_id, guild_id: guild_id, - ip: getIpAdress(req), + ip: req.ip, executor_id: req.user_id, reason: req.body.reason, // || otherwise empty }); diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts index 3d3ed1c39..a564e2d48 100644 --- a/src/api/routes/guilds/#guild_id/bulk-ban.ts +++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -81,7 +81,7 @@ router.post( const ban = Ban.create({ user_id: banned_user_id, guild_id: guild_id, - ip: getIpAdress(req), + ip: req.ip, executor_id: req.user_id, reason: req.body.reason, // || otherwise empty }); diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index a7fad818d..a3c5a81b6 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; +import { getVoiceRegions, route } from "@spacebar/api"; import { Guild } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -38,12 +38,7 @@ router.get( const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); //TODO we should use an enum for guild's features and not hardcoded strings - return res.json( - await getVoiceRegions( - getIpAdress(req), - guild.features.includes("VIP_REGIONS"), - ), - ); + return res.json(await getVoiceRegions(req.ip!, guild.features.includes("VIP_REGIONS"))); }, ); diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index c77d36eaa..87715f35e 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, PublicInviteRelation, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -83,7 +83,7 @@ router.post( const ban = await Ban.findOne({ where: [ { guild_id: guild_id, user_id: req.user_id }, - { guild_id: guild_id, ip: getIpAdress(req) }, + { guild_id: guild_id, ip: req.ip }, ], }); diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts index 9fdd6fdef..ba2612316 100644 --- a/src/api/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; +import { getVoiceRegions, route } from "@spacebar/api"; import { Request, Response, Router } from "express"; const router: Router = Router({ mergeParams: true }); @@ -31,7 +31,7 @@ router.get( }, }), async (req: Request, res: Response) => { - res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true? + res.json(await getVoiceRegions(req.ip!, true)); //vip true? }, ); diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts index 914999a71..19408253d 100644 --- a/src/api/util/utility/ipAddress.ts +++ b/src/api/util/utility/ipAddress.ts @@ -16,41 +16,16 @@ along with this program. If not, see . */ -import { Config } from "@spacebar/util"; -import { Request } from "express"; - -export function getIpAdress(req: Request): string { - // TODO: express can do this (trustProxies: true)? - - return req.ip!; -} - type Location = { latitude: number; longitude: number }; -export function distanceBetweenLocations( - loc1: Location, - loc2: Location, -): number { - return distanceBetweenCoords( - loc1.latitude, - loc1.longitude, - loc2.latitude, - loc2.longitude, - ); +export function distanceBetweenLocations(loc1: Location, loc2: Location): number { + return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude); } //Haversine function -function distanceBetweenCoords( - lat1: number, - lon1: number, - lat2: number, - lon2: number, -) { +function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) { const p = 0.017453292519943295; // Math.PI / 180 const c = Math.cos; - const a = - 0.5 - - c((lat2 - lat1) * p) / 2 + - (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; + const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km } From f78f3abe320df1c3f4ef7916c9c36e12bcd72f2a Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 11 Dec 2025 22:03:01 -0600 Subject: [PATCH 02/34] fix read del bug --- src/api/routes/channels/#channel_id/messages/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 8790241d8..43c4984e0 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -548,7 +548,7 @@ router.delete( // TODO: handle other read state types if (body.read_state_type != ReadStateType.CHANNEL) return res.status(204).send(); - const readState = await ReadState.findOne({where: {channel_id}}); + const readState = await ReadState.findOne({ where: { channel_id, user_id: req.user_id } }); if (readState) { await readState.remove(); } From d054febcf17e246bf0ccfbf7f244b6ad20a6965b Mon Sep 17 00:00:00 2001 From: dank074 Date: Tue, 9 Dec 2025 01:17:45 -0600 Subject: [PATCH 03/34] fix: prevent race condition in rabbitmq --- src/gateway/listener/listener.ts | 112 +++++++++++-------------------- src/gateway/util/WebSocket.ts | 4 +- src/util/util/Event.ts | 44 ++++-------- src/util/util/RabbitMQ.ts | 17 +++++ 4 files changed, 70 insertions(+), 107 deletions(-) diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index d7c3d23e5..75cb6bc61 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -36,7 +36,7 @@ import { WebSocket } from "@spacebar/gateway"; import { Channel as AMQChannel } from "amqplib"; import { Recipient } from "@spacebar/util"; import * as console from "node:console"; -import { PublicMember, RelationshipType } from "@spacebar/schemas" +import { PublicMember, RelationshipType } from "@spacebar/schemas"; import { bgRedBright } from "picocolors"; // TODO: close connection on Invalidated Token @@ -46,10 +46,7 @@ import { bgRedBright } from "picocolors"; // Sharding: calculate if the current shard id matches the formula: shard_id = (guild_id >> 22) % num_shards // https://discord.com/developers/docs/topics/gateway#sharding -export function handlePresenceUpdate( - this: WebSocket, - { event, acknowledge, data }: EventOpts, -) { +export function handlePresenceUpdate(this: WebSocket, { event, acknowledge, data }: EventOpts) { acknowledge?.(); if (event === EVENTEnum.PresenceUpdate) { return Send(this, { @@ -92,32 +89,24 @@ export async function setupListener(this: WebSocket) { this.listen_options = opts; const consumer = consume.bind(this); + const handleChannelError = (err: unknown) => { + console.error(`[RabbitMQ] [user-${this.user_id}] Channel Error (Handled):`, err); + }; + console.log("[RabbitMQ] setupListener: open for ", this.user_id); if (RabbitMQ.connection) { - console.log( - "[RabbitMQ] setupListener: opts.channel = ", - typeof opts.channel, - "with channel id", - opts.channel?.ch, - ); + console.log("[RabbitMQ] setupListener: opts.channel = ", typeof opts.channel, "with channel id", opts.channel?.ch); opts.channel = await RabbitMQ.connection.createChannel(); + + opts.channel.on("error", handleChannelError); opts.channel.queues = {}; - console.log( - "[RabbitMQ] channel created: ", - typeof opts.channel, - "with channel id", - opts.channel?.ch, - ); + console.log("[RabbitMQ] channel created: ", typeof opts.channel, "with channel id", opts.channel?.ch); } this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts); relationships.forEach(async (relationship) => { - this.events[relationship.to_id] = await listenEvent( - relationship.to_id, - handlePresenceUpdate.bind(this), - opts, - ); + this.events[relationship.to_id] = await listenEvent(relationship.to_id, handlePresenceUpdate.bind(this), opts); }); dm_channels.forEach(async (channel) => { @@ -130,33 +119,27 @@ export async function setupListener(this: WebSocket) { this.events[guild.id] = await listenEvent(guild.id, consumer, opts); guild.channels.forEach(async (channel) => { - if ( - permission - .overwriteChannel(channel.permission_overwrites ?? []) - .has("VIEW_CHANNEL") - ) { - this.events[channel.id] = await listenEvent( - channel.id, - consumer, - opts, - ); + if (permission.overwriteChannel(channel.permission_overwrites ?? []).has("VIEW_CHANNEL")) { + this.events[channel.id] = await listenEvent(channel.id, consumer, opts); } }); }); - this.once("close", () => { - console.log( - "[RabbitMQ] setupListener: close for", - this.user_id, - "=", - typeof opts.channel, - "with channel id", - opts.channel?.ch, + this.once("close", async () => { + console.log("[RabbitMQ] setupListener: close for", this.user_id, "=", typeof opts.channel, "with channel id", opts.channel?.ch); + + // wait for event consumer cancellation + await Promise.all( + Object.values(this.events).map((x) => { + if (x) return x(); + else return Promise.resolve(); + }), ); - if (opts.channel) opts.channel.close(); - else { - Object.values(this.events).forEach((x) => x?.()); - Object.values(this.member_events).forEach((x) => x()); + await Promise.all(Object.values(this.member_events).map((x) => x())); + + if (opts.channel) { + await opts.channel.close(); + opts.channel.off("error", handleChannelError); } }); } @@ -180,11 +163,7 @@ async function consume(this: WebSocket, opts: EventOpts) { break; case "GUILD_MEMBER_ADD": if (this.member_events[data.user.id]) break; // already subscribed - this.member_events[data.user.id] = await listenEvent( - data.user.id, - handlePresenceUpdate.bind(this), - this.listen_options, - ); + this.member_events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options); break; case "GUILD_MEMBER_UPDATE": if (!this.member_events[data.user.id]) break; @@ -197,32 +176,20 @@ async function consume(this: WebSocket, opts: EventOpts) { delete this.events[id]; break; case "CHANNEL_CREATE": - if ( - !permission - .overwriteChannel(data.permission_overwrites) - .has("VIEW_CHANNEL") - ) { + if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) { return; } this.events[id] = await listenEvent(id, consumer, listenOpts); break; case "RELATIONSHIP_ADD": - this.events[data.user.id] = await listenEvent( - data.user.id, - handlePresenceUpdate.bind(this), - this.listen_options, - ); + this.events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options); break; case "GUILD_CREATE": this.events[id] = await listenEvent(id, consumer, listenOpts); break; case "CHANNEL_UPDATE": { const exists = this.events[id]; - if ( - permission - .overwriteChannel(data.permission_overwrites) - .has("VIEW_CHANNEL") - ) { + if (permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) { if (exists) break; this.events[id] = await listenEvent(id, consumer, listenOpts); } else { @@ -294,20 +261,19 @@ async function consume(this: WebSocket, opts: EventOpts) { case "MESSAGE_UPDATE": // console.log(this.request) if (data["attachments"]) - data["attachments"] = - Message.prototype.withSignedAttachments.call( - data, - new NewUrlUserSignatureData({ - ip: this.ipAddress, - userAgent: this.userAgent, - }), - ).attachments; + data["attachments"] = Message.prototype.withSignedAttachments.call( + data, + new NewUrlUserSignatureData({ + ip: this.ipAddress, + userAgent: this.userAgent, + }), + ).attachments; break; default: break; } - if(event === "GUILD_MEMBER_ADD") { + if (event === "GUILD_MEMBER_ADD") { if ((data as PublicMember).roles === undefined || (data as PublicMember).roles === null) { console.log(bgRedBright("[Gateway]"), "[GUILD_MEMBER_ADD] roles is undefined, setting to empty array!", opts.origin ?? "(Event origin not defined)", data); (data as PublicMember).roles = []; diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index ea6f0701e..00ef9e98f 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -44,8 +44,8 @@ export interface WebSocket extends WS { intents: Intents; sequence: number; permissions: Record; - events: Record unknown)>; - member_events: Record unknown>; + events: Record Promise)>; + member_events: Record Promise>; listen_options: ListenEventOpts; capabilities?: Capabilities; large_threshold: number; diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts index f56d66646..ce5c25d25 100644 --- a/src/util/util/Event.ts +++ b/src/util/util/Event.ts @@ -20,30 +20,21 @@ import { Channel } from "amqplib"; import { RabbitMQ } from "./RabbitMQ"; import EventEmitter from "events"; import { EVENT, Event } from "../interfaces"; +import { randomUUID } from "crypto"; export const events = new EventEmitter(); export async function emitEvent(payload: Omit) { - const id = (payload.guild_id || - payload.channel_id || - payload.user_id) as string; + const id = (payload.guild_id || payload.channel_id || payload.user_id) as string; if (!id) return console.error("event doesn't contain any id", payload); if (RabbitMQ.connection) { - const data = - typeof payload.data === "object" - ? JSON.stringify(payload.data) - : payload.data; // use rabbitmq for event transmission + const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission await RabbitMQ.channel?.assertExchange(id, "fanout", { durable: false, }); // assertQueue isn't needed, because a queue will automatically created if it doesn't exist - const successful = RabbitMQ.channel?.publish( - id, - "", - Buffer.from(`${data}`), - { type: payload.event }, - ); + const successful = RabbitMQ.channel?.publish(id, "", Buffer.from(`${data}`), { type: payload.event }); if (!successful) throw new Error("failed to send event"); } else if (process.env.EVENT_TRANSMISSION === "process") { process.send?.({ type: "event", event: payload, id } as ProcessEvent); @@ -79,17 +70,10 @@ export interface ProcessEvent { id: string; } -export async function listenEvent( - event: string, - callback: (event: EventOpts) => unknown, - opts?: ListenEventOpts, -) { +export async function listenEvent(event: string, callback: (event: EventOpts) => unknown, opts?: ListenEventOpts): Promise<() => Promise> { if (RabbitMQ.connection) { const channel = opts?.channel || RabbitMQ.channel; - if (!channel) - throw new Error( - "[Events] An event was sent without an associated channel", - ); + if (!channel) throw new Error("[Events] An event was sent without an associated channel"); return await rabbitListen(channel, event, callback, { acknowledge: opts?.acknowledge, }); @@ -101,9 +85,7 @@ export async function listenEvent( const listener = (msg: ProcessEvent) => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - msg.type === "event" && - msg.id === event && - callback({ ...msg.event, cancel }); + msg.type === "event" && msg.id === event && callback({ ...msg.event, cancel }); }; // TODO: assert the type is correct? @@ -124,20 +106,17 @@ export async function listenEvent( } } -async function rabbitListen( - channel: Channel, - id: string, - callback: (event: EventOpts) => unknown, - opts?: { acknowledge?: boolean }, -) { +async function rabbitListen(channel: Channel, id: string, callback: (event: EventOpts) => unknown, opts?: { acknowledge?: boolean }): Promise<() => Promise> { await channel.assertExchange(id, "fanout", { durable: false }); const q = await channel.assertQueue("", { exclusive: true, autoDelete: true, }); + const consumerTag = randomUUID(); + const cancel = async () => { - await channel.cancel(q.queue); + await channel.cancel(consumerTag); await channel.unbindQueue(q.queue, id, ""); }; @@ -163,6 +142,7 @@ async function rabbitListen( }, { noAck: !opts?.acknowledge, + consumerTag: consumerTag, }, ); diff --git a/src/util/util/RabbitMQ.ts b/src/util/util/RabbitMQ.ts index 1a61aee93..89e8e140c 100644 --- a/src/util/util/RabbitMQ.ts +++ b/src/util/util/RabbitMQ.ts @@ -34,7 +34,24 @@ export const RabbitMQ: { timeout: 1000 * 60, }); console.log(`[RabbitMQ] connected`); + + // log connection errors + this.connection.on("error", (err) => { + console.error("[RabbitMQ] Connection Error:", err); + }); + + this.connection.on("close", () => { + console.error("[RabbitMQ] connection closed"); + // TODO: Add reconnection logic here if the connection crashes?? + // will be a pain since we will have to reconstruct entire state + }); + this.channel = await this.connection.createChannel(); console.log(`[RabbitMQ] channel created`); + + // log channel errors + this.channel.on("error", (err) => { + console.error("[RabbitMQ] Channel Error:", err); + }); }, }; From eb94dd4b16860aa6932685c541444e9f12136411 Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Fri, 5 Dec 2025 01:17:26 +0530 Subject: [PATCH 04/34] Refactor: Replace getIpAddress with req.ip --- assets/openapi.json | Bin 856618 -> 856618 bytes assets/schemas.json | Bin 380340 -> 380340 bytes src/api/middlewares/RateLimit.ts | 48 ++++---------------- src/api/routes/auth/forgot.ts | 15 ++---- src/api/routes/auth/location-metadata.ts | 4 +- src/api/routes/auth/login.ts | 45 ++++-------------- src/api/routes/auth/register.ts | 8 ++-- src/api/routes/auth/verify/index.ts | 12 ++--- src/api/routes/guilds/#guild_id/bans.ts | 4 +- src/api/routes/guilds/#guild_id/bulk-ban.ts | 4 +- src/api/routes/guilds/#guild_id/regions.ts | 9 +--- src/api/routes/invites/index.ts | 4 +- src/api/routes/voice/regions.ts | 4 +- src/api/util/utility/ipAddress.ts | 33 ++------------ 14 files changed, 48 insertions(+), 142 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index e502af61c79f8dbc67f1737ccd1347040b5b01a3..07175b06584c671407de972ea93fdb6ff1bc306e 100644 GIT binary patch delta 283 zcmZ3r#AMYHlMPNR)A#RZ7HXc!x_u@qa8f~nT@4c1In(^pEXq-P7kzW#b2H0rFC&iL%R1zc7bM4n$Z@4-jY6LMSL?WKn2;GK~$0*@2h? Sh&h3n3y8V5KbgkkRt*4nfo9nN diff --git a/assets/schemas.json b/assets/schemas.json index d5b4835ef2fae21b650726a5e2eeeec4a12d1736..24eebc3b516a584e8e5143d502db52a110fcb299 100644 GIT binary patch delta 324 zcmdloTYSrGamMQDiq6au&Do6Gvl$tst0ph_BC)-pnDH3n^bb=Q*``0JWLBO0p;B)8 zgC0h&=?3bItkVs4Gl@-pz^OC6VK=khbOjC;x#K(%fED!^$Jf zWNt9|pp+g+f$Q`O6Il7dra?uZS{67lX-?m8i&1!bfHk8MLRBHB#P(kkSa;a~0K{N& AL;wH) delta 394 zcmdloTYSrGamMQHx2>477#k(GN-|1UP4Craa^J4d$ymrVyfr_bkRcAQqp$TvN}nbmRnhf6HH+b@(cZeg6>u!==s z`o3w*+}qDhU<_uNKKCFa`*xc)#wScvx1k+q1JD*G;Z@TWrZRF*@1M%TK3&$HnZ4b| zh8c)iw)@zyZna}Jv$U8z@rBy-hID47=?9iDnN8jmr#;z$kz@MnN>(*6!@-kPZhAlq ztKf8nAV$$?JS@`F8+e)7COb%KZ!fN4Or^o}3lmuRL1v?hLrj=1 gu$oZ}LVU. */ -import { getIpAdress } from "@spacebar/api"; import { Config, getRights, listenEvent } from "@spacebar/util"; import { NextFunction, Request, Response, Router } from "express"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; @@ -65,21 +64,14 @@ export default function rateLimit(opts: { if (rights.has("BYPASS_RATE_LIMITS")) return next(); } - const bucket_id = - opts.bucket || - req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); - let executor_id = getIpAdress(req); + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); + let executor_id = req.ip || "127.0.0.1"; if (!opts.onlyIp && req.user_id) executor_id = req.user_id; let max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; - if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) - max_hits = opts.GET; - else if ( - opts.MODIFY && - ["POST", "DELETE", "PATCH", "PUT"].includes(req.method) - ) - max_hits = opts.MODIFY; + if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; + else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; const offender = Cache.get(executor_id + bucket_id); @@ -104,18 +96,13 @@ export default function rateLimit(opts: { } res.set("X-RateLimit-Reset", `${reset}`); - res.set( - "X-RateLimit-Reset-After", - `${Math.max(0, Math.ceil(resetAfterSec))}`, - ); + res.set("X-RateLimit-Reset-After", `${Math.max(0, Math.ceil(resetAfterSec))}`); if (offender.blocked) { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further reset += opts.window * 1000; - offender.expires_at = new Date( - offender.expires_at.getTime() + opts.window * 1000, - ); + offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); @@ -129,10 +116,7 @@ export default function rateLimit(opts: { res .status(429) .set("X-RateLimit-Remaining", "0") - .set( - "Retry-After", - `${Math.max(0, Math.ceil(resetAfterSec))}`, - ) + .set("Retry-After", `${Math.max(0, Math.ceil(resetAfterSec))}`) // TODO: error rate limit message translation .send({ message: "You are being rate limited.", @@ -156,11 +140,7 @@ export default function rateLimit(opts: { // check if error and increment error rate limit if (res.statusCode >= 400 && opts.error) { return hitRoute(hitRouteOpts); - } else if ( - res.statusCode >= 200 && - res.statusCode < 300 && - opts.success - ) { + } else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) { return hitRoute(hitRouteOpts); } }); @@ -213,18 +193,10 @@ export async function initRateLimits(app: Router) { app.use("/webhooks/:webhook_id", rateLimit(routes.webhook)); app.use("/channels/:channel_id", rateLimit(routes.channel)); app.use("/auth/login", rateLimit(routes.auth.login)); - app.use( - "/auth/register", - rateLimit({ onlyIp: true, success: true, ...routes.auth.register }), - ); + app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { - executor_id: string; - bucket_id: string; - max_hits: number; - window: number; -}) { +async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { const id = opts.executor_id + opts.bucket_id; let limit = Cache.get(id); if (!limit) { diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts index 12bc9cb07..6e613820f 100644 --- a/src/api/routes/auth/forgot.ts +++ b/src/api/routes/auth/forgot.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; +import { route, verifyCaptcha } from "@spacebar/api"; import { Config, Email, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { ForgotPasswordSchema } from "@spacebar/schemas" +import { ForgotPasswordSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.post( @@ -38,10 +38,7 @@ router.post( const config = Config.get(); - if ( - config.passwordReset.requireCaptcha && - config.security.captcha.enabled - ) { + if (config.passwordReset.requireCaptcha && config.security.captcha.enabled) { const { sitekey, service } = config.security.captcha; if (!captcha_key) { return res.status(400).json({ @@ -51,7 +48,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ @@ -71,9 +68,7 @@ router.post( if (user && user.email) { Email.sendResetPassword(user, user.email).catch((e) => { - console.error( - `Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`, - ); + console.error(`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`); }); } }, diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index a08c98abe..92f47909d 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { IpDataClient } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); @@ -33,7 +33,7 @@ router.get( async (req: Request, res: Response) => { //TODO //Note: It's most likely related to legal. At the moment Discord hasn't finished this too - const country_code = (await IpDataClient.getIpInfo(getIpAdress(req)))?.country_code; + const country_code = (await IpDataClient.getIpInfo(req.ip!))?.country_code; res.json({ consent_required: false, country_code: country_code, diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 06e3fe43e..c8ef62731 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -16,19 +16,12 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; -import { - Config, - FieldErrors, - User, - WebAuthn, - generateToken, - generateWebAuthnTicket, -} from "@spacebar/util"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { Config, FieldErrors, User, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util"; import bcrypt from "bcrypt"; import crypto from "crypto"; import { Request, Response, Router } from "express"; -import { LoginSchema } from "@spacebar/schemas" +import { LoginSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); export default router; @@ -61,7 +54,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ @@ -74,17 +67,7 @@ router.post( const user = await User.findOneOrFail({ where: [{ phone: login }, { email: login }], - select: [ - "data", - "id", - "disabled", - "deleted", - "totp_secret", - "mfa_enabled", - "webauthn_enabled", - "security_keys", - "verified", - ], + select: ["data", "id", "disabled", "deleted", "totp_secret", "mfa_enabled", "webauthn_enabled", "security_keys", "verified"], relations: ["security_keys", "settings"], }).catch(() => { throw FieldErrors({ @@ -100,10 +83,7 @@ router.post( }); // the salt is saved in the password refer to bcrypt docs - const same_password = await bcrypt.compare( - password, - user.data.hash || "", - ); + const same_password = await bcrypt.compare(password, user.data.hash || ""); if (!same_password) { throw FieldErrors({ login: { @@ -122,8 +102,7 @@ router.post( throw FieldErrors({ login: { code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL", - message: - "Email verification is required, please check your email.", + message: "Email verification is required, please check your email.", }, }); } @@ -152,9 +131,7 @@ router.post( const challenge = JSON.stringify({ publicKey: { ...options, - challenge: Buffer.from(options.challenge).toString( - "base64", - ), + challenge: Buffer.from(options.challenge).toString("base64"), allowCredentials: user.security_keys.map((x) => ({ id: x.key_id, type: "public-key", @@ -178,10 +155,8 @@ router.post( if (undelete) { // undelete refers to un'disable' here - if (user.disabled) - await User.update({ id: user.id }, { disabled: false }); - if (user.deleted) - await User.update({ id: user.id }, { deleted: false }); + if (user.disabled) await User.update({ id: user.id }, { disabled: false }); + if (user.deleted) await User.update({ id: user.id }, { deleted: false }); } else { if (user.deleted) return res.status(400).json({ diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 418f0a529..d4b061be2 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; +import { route, verifyCaptcha } from "@spacebar/api"; import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; @@ -38,7 +38,7 @@ router.post( async (req: Request, res: Response) => { const body = req.body as RegisterSchema; const { register, security, limits } = Config.get(); - const ip = getIpAdress(req); + const ip = req.ip!; // Reg tokens // They're a one time use token that bypasses registration limits ( rates, disabled reg, etc ) @@ -143,7 +143,7 @@ router.post( const ipData = await IpDataClient.getIpInfo(ip); if (ipData) { - if(!ipData.threat) { + if (!ipData.threat) { console.log("Invalid IPData.co response, missing threat field", ipData); } const categories = Object.entries(ipData.threat) @@ -287,7 +287,7 @@ router.post( }, })) >= limits.absoluteRate.register.limit ) { - console.log(`Global register ratelimit exceeded for ${getIpAdress(req)}, ${req.body.username}, ${req.body.invite || "No invite given"}`); + console.log(`Global register ratelimit exceeded for ${req.ip}, ${req.body.username}, ${req.body.invite || "No invite given"}`); throw FieldErrors({ email: { code: "TOO_MANY_REGISTRATIONS", diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index b85120d84..d8e343cd2 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -16,14 +16,8 @@ along with this program. If not, see . */ -import { getIpAdress, route, verifyCaptcha } from "@spacebar/api"; -import { - checkToken, - Config, - FieldErrors, - generateToken, - User, -} from "@spacebar/util"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { checkToken, Config, FieldErrors, generateToken, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); @@ -67,7 +61,7 @@ router.post( }); } - const ip = getIpAdress(req); + const ip = req.ip; const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 1274c195d..30ef8df7b 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -279,4 +279,4 @@ router.delete( }, ); -export default router; +export default router; \ No newline at end of file diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts index 3d3ed1c39..a564e2d48 100644 --- a/src/api/routes/guilds/#guild_id/bulk-ban.ts +++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -81,7 +81,7 @@ router.post( const ban = Ban.create({ user_id: banned_user_id, guild_id: guild_id, - ip: getIpAdress(req), + ip: req.ip, executor_id: req.user_id, reason: req.body.reason, // || otherwise empty }); diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index a7fad818d..a3c5a81b6 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; +import { getVoiceRegions, route } from "@spacebar/api"; import { Guild } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -38,12 +38,7 @@ router.get( const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); //TODO we should use an enum for guild's features and not hardcoded strings - return res.json( - await getVoiceRegions( - getIpAdress(req), - guild.features.includes("VIP_REGIONS"), - ), - ); + return res.json(await getVoiceRegions(req.ip!, guild.features.includes("VIP_REGIONS"))); }, ); diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index c77d36eaa..87715f35e 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, route } from "@spacebar/api"; +import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, PublicInviteRelation, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -83,7 +83,7 @@ router.post( const ban = await Ban.findOne({ where: [ { guild_id: guild_id, user_id: req.user_id }, - { guild_id: guild_id, ip: getIpAdress(req) }, + { guild_id: guild_id, ip: req.ip }, ], }); diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts index 9fdd6fdef..ba2612316 100644 --- a/src/api/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; +import { getVoiceRegions, route } from "@spacebar/api"; import { Request, Response, Router } from "express"; const router: Router = Router({ mergeParams: true }); @@ -31,7 +31,7 @@ router.get( }, }), async (req: Request, res: Response) => { - res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true? + res.json(await getVoiceRegions(req.ip!, true)); //vip true? }, ); diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts index 914999a71..19408253d 100644 --- a/src/api/util/utility/ipAddress.ts +++ b/src/api/util/utility/ipAddress.ts @@ -16,41 +16,16 @@ along with this program. If not, see . */ -import { Config } from "@spacebar/util"; -import { Request } from "express"; - -export function getIpAdress(req: Request): string { - // TODO: express can do this (trustProxies: true)? - - return req.ip!; -} - type Location = { latitude: number; longitude: number }; -export function distanceBetweenLocations( - loc1: Location, - loc2: Location, -): number { - return distanceBetweenCoords( - loc1.latitude, - loc1.longitude, - loc2.latitude, - loc2.longitude, - ); +export function distanceBetweenLocations(loc1: Location, loc2: Location): number { + return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude); } //Haversine function -function distanceBetweenCoords( - lat1: number, - lon1: number, - lat2: number, - lon2: number, -) { +function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) { const p = 0.017453292519943295; // Math.PI / 180 const c = Math.cos; - const a = - 0.5 - - c((lat2 - lat1) * p) / 2 + - (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; + const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km } From bdbf6d06b25fcdb2e9cbcdd32872ae62bfc8893c Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 7 Dec 2025 23:18:57 +0100 Subject: [PATCH 05/34] Fix IP tracking for bans --- src/api/routes/guilds/#guild_id/bans.ts | 13 ++++- src/gateway/Server.ts | 5 ++ src/gateway/events/Connection.ts | 7 +++ src/gateway/listener/listener.ts | 11 ++++ src/gateway/util/WebSocket.ts | 1 + src/schemas/uncategorised/BanCreateSchema.ts | 4 +- src/util/entities/InstanceBan.ts | 57 ++++++++++++++++++++ src/util/interfaces/Event.ts | 1 + 8 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/util/entities/InstanceBan.ts diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 30ef8df7b..39eee34aa 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -20,7 +20,7 @@ import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { APIBansArray, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas"; +import { APIBansArray, BanCreateSchema, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); @@ -198,6 +198,15 @@ router.put( async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user_id = req.params.user_id; + const opts = req.body as BanCreateSchema; + + let deleteMessagesMs = opts.delete_message_days + ? (opts.delete_message_days as number) * 86400000 + : opts.delete_message_seconds + ? (opts.delete_message_seconds as number) * 1000 + : 0; + + if (deleteMessagesMs < 0) deleteMessagesMs = 0; if (req.user_id === banned_user_id && banned_user_id === req.permission?.cache.guild?.owner_id) throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); @@ -207,6 +216,7 @@ router.put( const existingBan = await Ban.findOne({ where: { guild_id: guild_id, user_id: banned_user_id }, }); + // Bans on already banned users are silently ignored if (existingBan) return res.status(204).send(); @@ -227,6 +237,7 @@ router.put( data: { guild_id: guild_id, user: banned_user.toPublicUser(), + delete_message_secs: Math.floor(deleteMessagesMs / 1000), }, guild_id: guild_id, } as GuildBanAddEvent), diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts index 269cc2ed2..5d7f8d87a 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts @@ -28,6 +28,7 @@ import ws from "ws"; import { Connection } from "./events/Connection"; import http from "http"; import { cleanupOnStartup } from "./util/Utils"; +import { randomString } from "@spacebar/api"; export class Server { public ws: ws.Server; @@ -50,6 +51,10 @@ export class Server { if (server) this.server = server; else { this.server = http.createServer(function (req, res) { + if(!req.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) { + res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None`); + } + res.writeHead(200).end("Online"); }); } diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts index bc74dee65..9b132d4b9 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts @@ -72,6 +72,13 @@ export async function Connection( ); } + if (request.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) { + socket.fingerprint = request.headers.cookie + .split("; ") + .find((x) => x.startsWith("__sb_sessid=")) + ?.split("=")[1]; + } + //Create session ID when the connection is opened. This allows gateway dump to group the initial websocket messages with the rest of the conversation. const session_id = genSessionId(); socket.session_id = session_id; //Set the session of the WebSocket object diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 75cb6bc61..f76adfb29 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -29,6 +29,7 @@ import { Message, NewUrlUserSignatureData, GuildMemberAddEvent, + Ban, } from "@spacebar/util"; import { OPCODES } from "../util/Constants"; import { Send } from "../util/Send"; @@ -174,6 +175,16 @@ async function consume(this: WebSocket, opts: EventOpts) { case "GUILD_DELETE": this.events[id]?.(); delete this.events[id]; + if (event === "GUILD_DELETE" && this.ipAddress) { + const ban = await Ban.findOne({ + where: { guild_id: id, user_id: this.user_id }, + }); + + if (ban) { + ban.ip = this.ipAddress || undefined; + await ban.save(); + } + } break; case "CHANNEL_CREATE": if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) { diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 00ef9e98f..d0a42eafa 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -33,6 +33,7 @@ export interface WebSocket extends WS { compress?: "zlib-stream" | "zstd-stream"; ipAddress?: string; userAgent?: string; // for cdn request signing + fingerprint?: string; shard_count?: bigint; shard_id?: bigint; deflate?: Deflate; diff --git a/src/schemas/uncategorised/BanCreateSchema.ts b/src/schemas/uncategorised/BanCreateSchema.ts index 6d30ca221..8f349f4bf 100644 --- a/src/schemas/uncategorised/BanCreateSchema.ts +++ b/src/schemas/uncategorised/BanCreateSchema.ts @@ -17,7 +17,7 @@ */ export interface BanCreateSchema { - delete_message_seconds?: string; - delete_message_days?: string; + delete_message_seconds?: number; + delete_message_days?: number; reason?: string; } diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts new file mode 100644 index 000000000..8f8cd29d2 --- /dev/null +++ b/src/util/entities/InstanceBan.ts @@ -0,0 +1,57 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Team } from "./Team"; +import { User } from "./User"; +import { Guild } from "./Guild"; + +@Entity({ + name: "instance_bans", +}) +export class InstanceBan extends BaseClass { + @Column({ type: "bigint" }) + @CreateDateColumn() + created_at: Date = new Date(); + + @Column() + reason: string; + + @Column({ nullable: true }) + user_id: string; + + @Column({ nullable: true }) + fingerprint: string; + + @Column({ nullable: true }) + ip_address: string; + + // chain of trust type tracking + + @Column() + is_from_other_instance_ban: boolean; + + @Column({ nullable: true }) + @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban) + origin_instance_ban_id?: string; + + @JoinColumn({ name: "origin_instance_ban_id" }) + @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" }) + origin_instance_ban?: InstanceBan; +} diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 24157efd1..d0a604937 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -219,6 +219,7 @@ export interface GuildBanAddEvent extends Event { data: { guild_id: string; user: User; + delete_message_secs?: number }; } From 1573e5a1d9bb4863cd7278e75bc7d9ada1b89775 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 12 Dec 2025 23:28:20 +0100 Subject: [PATCH 06/34] Instance ban: clean up DMs and guild ownerships, track instance bans --- src/api/routes/users/#user_id/delete.ts | 124 +++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index dbb18e290..277aeb386 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -17,9 +17,10 @@ */ import { route } from "@spacebar/api"; -import { emitEvent, Member, User, UserDeleteEvent } from "@spacebar/util"; +import { Channel, ChannelDeleteEvent, ChannelRecipientRemoveEvent, emitEvent, Emoji, Guild, InstanceBan, Member, Recipient, Sticker, User, UserDeleteEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { PrivateUserProjection } from "@spacebar/schemas"; +import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas"; +import { Not } from "typeorm"; const router = Router({ mergeParams: true }); @@ -27,6 +28,7 @@ router.post( "/", route({ right: "MANAGE_USERS", + requestBody: "InstanceUserDeleteSchema", responses: { 204: {}, 403: { @@ -38,10 +40,126 @@ router.post( }, }), async (req: Request, res: Response) => { - await User.findOneOrFail({ + const body = req.body as InstanceUserDeleteSchema | undefined; + const user = await User.findOneOrFail({ where: { id: req.params.user_id }, select: [...PrivateUserProjection, "data"], }); + + await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "" }).save(); + + // prevent bugginess with clients - delete all DMs, only having half of the conversation is quite useless anyhow + const dmChannels = await user.getDmChannels(); + for (const channel of dmChannels) { + console.log(`[Instance ban] Deleting DM channel ${channel.id} for user ${user.id}`); + await emitEvent({ + event: "CHANNEL_DELETE", + data: channel.toJSON(), + channel_id: channel.id, + } as ChannelDeleteEvent); + await Recipient.delete({ channel_id: channel.id }); + await Channel.deleteChannel(channel); + } + + //leave all group channels + const groupChannels = await Channel.find({ + where: { type: ChannelType.GROUP_DM }, + relations: ["recipients"], + select: { + id: true, + owner_id: true, + recipients: { + id: true, + user_id: true, + }, + }, + }); + + await Promise.all( + groupChannels.map(async (channel) => { + const recipient = channel.recipients!.find((r) => r.user_id === user.id); + if (recipient) { + await Recipient.delete({ id: recipient.id }); + await emitEvent({ + event: "CHANNEL_RECIPIENT_REMOVE", + data: { + user: user.toPublicUser(), + channel_id: channel.id, + }, + channel_id: channel.id, + } as ChannelRecipientRemoveEvent); + console.log(`[Instance ban] Removed user ${user.id} from group channel ${channel.id}`); + } + + // if no recipients remain, delete the channel + const remainingRecipients = await Recipient.find({ where: { channel_id: channel.id } }); + if (remainingRecipients.length === 0) { + await emitEvent({ + event: "CHANNEL_DELETE", + data: channel.toJSON(), + channel_id: channel.id, + } as ChannelDeleteEvent); + await Channel.deleteChannel(channel); + console.log(`[Instance ban] Deleted empty group channel ${channel.id}`); + } else { + // otherwise, if the banned user was the owner, reassign ownership + if (channel.owner_id === user.id) { + channel.owner_id = remainingRecipients[0].user_id; + await channel.save(); + console.log(`[Instance ban] Reassigned ownership of group channel ${channel.id} to user ${channel.owner_id}`); + } + } + }), + ); + + // change ownership on guilds + const guilds = await Guild.find({ where: { owner_id: req.params.user_id } }); + await Promise.all( + guilds.map(async (guild) => { + const members = await Member.find({ + where: { guild_id: guild.id, id: Not(req.params.user_id) }, + relations: { roles: true }, + select: { id: true, roles: { id: true, position: true } }, + }); + const sortedMembers = members + .filter((m) => m.id !== req.params.user_id) + .sort((a, b) => { + const aHighestRole = a.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number }); + const bHighestRole = b.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number }); + return bHighestRole.position - aHighestRole.position; + }); + if (sortedMembers.length === 0) { + // no members left, delete guild + await guild.remove(); + console.log(`[Instance ban] Deleted guild ${guild.id} as user ${user.id} was the last member`); + } else { + // assign new owner + guild.owner_id = sortedMembers[0].id; + await guild.save(); + console.log(`[Instance ban] Transferred ownership of guild ${guild.id} to user ${guild.owner_id}`); + + // safety - reassign emojis/stickers owned by the old owner + const stickers = await Sticker.find({ where: { guild_id: guild.id, user_id: req.params.user_id } }); + await Promise.all( + stickers.map(async (sticker) => { + sticker.user_id = guild.owner_id; + await sticker.save(); + console.log(`[Instance ban] Reassigned sticker ${sticker.id} ownership to user ${guild.owner_id}`); + }), + ); + + const emojis = await Emoji.find({ where: { guild_id: guild.id, user_id: req.params.user_id } }); + await Promise.all( + emojis.map(async (emoji) => { + emoji.user_id = guild.owner_id!; + await emoji.save(); + console.log(`[Instance ban] Reassigned emoji ${emoji.id} ownership to user ${guild.owner_id}`); + }), + ); + } + }), + ); + const members = await Member.find({ where: { id: req.params.user_id } }); await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id)), User.delete({ id: req.params.user_id })]); From 811c1a25a9e75721be6936b7d6d33b11a9c6d7ff Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 12 Dec 2025 23:31:09 +0100 Subject: [PATCH 07/34] More instance ban stuff --- assets/openapi.json | Bin 856618 -> 857374 bytes assets/schemas.json | Bin 380340 -> 380789 bytes .../api/users/InstanceUserDeleteSchema.ts | 6 ++++++ src/schemas/api/users/index.ts | 1 + src/util/entities/Channel.ts | 14 ++++++++------ src/util/entities/InstanceBan.ts | 10 +++++----- src/util/entities/User.ts | 13 +++++++++++++ src/util/entities/index.ts | 1 + .../1765578570423-InstanceBanTable.ts | 16 ++++++++++++++++ 9 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 src/schemas/api/users/InstanceUserDeleteSchema.ts create mode 100644 src/util/migration/postgres/1765578570423-InstanceBanTable.ts diff --git a/assets/openapi.json b/assets/openapi.json index 07175b06584c671407de972ea93fdb6ff1bc306e..83138115d0e0dfb750f49e21f06922c2ef2b3514 100644 GIT binary patch delta 462 zcmZ3r#AMzolMPNR+y9EEN7w7@)rB&O);0T&r%+t0=@Z9f~sY>+cOUYV79y8Q)a-u9?d%-f?*v26PV z_M0GvN2WV+vrA7_*u^=ypww)1;|U8Cdw{5{lXle zc|gKydVn~i7D7QGBa1@&lWA<*pG;#vzhL@>D)yH=o_UA>->$Hdn~8CIz?>|&q(UzCsZD#iA(_2}i zryG1`R-Are6SsVO!)fO24X0V8er@kP##+6d(R{k0F{k{rKt|5#7iKf-P8YD}WS#6_ zVL!dHhRp~hy!~z++Xiu8z2a??MEGfGbWuvQkt5SadrW-JFNKbE2W|y9Bpv0&J*2ipaU_808P+@vQEgO4##tgRY88g^VEZAP4 f!zI8pz2G9JWcvjh4j|?PVlE)&-hRP`$L|IJl7yR` diff --git a/assets/schemas.json b/assets/schemas.json index 24eebc3b516a584e8e5143d502db52a110fcb299..4bf97dc42c9a3c5d434a7d3bf8480750f9832dd2 100644 GIT binary patch delta 538 zcmdloTm0)hamMQHx2>477&l6)wF!9U6_+IDC8vfKrxv-S=A=#!EMO9w%vCI=fG$&# z8l0Swnw#jHpI4HaSE4li;aNui&CYEfKTq$~W^&)I(8*ZHG`(Rrv)<$d+;YYa;-mr>A zVEVpk%-q}0O<)XWnLhU*Bl~unHpVARG_m1--sB5^)u+$?&&WH4NeReSoh+#)Jt-+_ z^85^k*56E9e=|R`oUSmHk$ZaoR2KH>vi8jE?LIcl+kI?Uf)~i(3!mwRH7xE(rfr{p zhb3tqvzevET37|AD+Do$ zPUB&bp5DOA%r@CUQhR%G4J)@Wlcm}8hyIMJ(_tzNreB!A$`3LdRUBf%bb-~3Vi4j( Z2D8ZY3+sSZ!!%4ToGiKh_XO6RHUJ&~#Qy*Q delta 297 zcmex5PkhU4amMQD_Z*oeHfJ-cwN3xQV-n_o$9^>?YTvm_C8+zC$ z*Q<$5UeKpEJ+6n@efs%!M$YZ;>KL_owkLEm7BZoVI&e<+o59GnU1d7sqO9o$UNEap zUoefCb^1p}Chh4HOqf)s$Fnn)Ojo$a;?Vw$iD~;cCgx|B8`)DB+uzwRZ+~aQ618A^ z!abIxdCcYp#?uwe7}X{>Xemu=U^1J`lb}6WVKe9SwbiVu(+m1p&8I6IVUnD@z*c5* z!g1~G#&xVb!c67{(-T`*i-qFj9ZK_xlk*D_lX6mv9YBTw#Y4baraQ. */ export * from "./ConnectedAccount"; +export * from "./InstanceUserDeleteSchema"; export * from "./Member"; export * from "./User"; export * from "./UserSettings"; \ No newline at end of file diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 782420447..e21669eae 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -379,13 +379,15 @@ export class Channel extends BaseClass { // TODO Delete attachments from the CDN for messages in the channel await Channel.delete({ id: channel.id }); - const guild = await Guild.findOneOrFail({ - where: { id: channel.guild_id }, - select: { channel_ordering: true }, - }); + if (channel.guild_id) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + select: { channel_ordering: true }, + }); - const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id); - await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering }); + const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id); + await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering }); + } } static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) { diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts index 8f8cd29d2..6f92e2ecc 100644 --- a/src/util/entities/InstanceBan.ts +++ b/src/util/entities/InstanceBan.ts @@ -34,18 +34,18 @@ export class InstanceBan extends BaseClass { reason: string; @Column({ nullable: true }) - user_id: string; + user_id?: string; @Column({ nullable: true }) - fingerprint: string; + fingerprint?: string; @Column({ nullable: true }) - ip_address: string; + ip_address?: string; // chain of trust type tracking - @Column() - is_from_other_instance_ban: boolean; + @Column({ default: false }) + is_from_other_instance_ban: boolean = false; @Column({ nullable: true }) @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index afa7c00e4..18d0f805f 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -359,4 +359,17 @@ export class User extends BaseClass { return qry[0]; } + + async getDmChannels() { + const qry = await Channel.getRepository() + .createQueryBuilder() + .leftJoinAndSelect("Channel.recipients", "rcp") + .where("Channel.type = :type", { types: ChannelType.DM }) + .andWhere("rcp.user_id = :user_id", { user_id: this.id }) + .groupBy("Channel.id") + .having("COUNT(rcp.user_id) = 2") + .getMany(); + + return qry; + } } diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 11494d00d..0e67675a8 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -36,6 +36,7 @@ export * from "./EmbedCache"; export * from "./Emoji"; export * from "./Encryption"; export * from "./Guild"; +export * from "./InstanceBan"; export * from "./Invite"; export * from "./Member"; export * from "./Message"; diff --git a/src/util/migration/postgres/1765578570423-InstanceBanTable.ts b/src/util/migration/postgres/1765578570423-InstanceBanTable.ts new file mode 100644 index 000000000..0baa7dc9c --- /dev/null +++ b/src/util/migration/postgres/1765578570423-InstanceBanTable.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InstanceBanTable1765578570423 implements MigrationInterface { + name = 'InstanceBanTable1765578570423' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "instance_bans" ("id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "reason" character varying NOT NULL, "user_id" character varying, "fingerprint" character varying, "ip_address" character varying, "is_from_other_instance_ban" boolean NOT NULL DEFAULT false, "origin_instance_ban_id" character varying, CONSTRAINT "REL_0b02d18d0d830f160c921192a3" UNIQUE ("origin_instance_ban_id"), CONSTRAINT "PK_3aa6e80a6d325601054892b1340" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "instance_bans" ADD CONSTRAINT "FK_0b02d18d0d830f160c921192a30" FOREIGN KEY ("origin_instance_ban_id") REFERENCES "instance_bans"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance_bans" DROP CONSTRAINT "FK_0b02d18d0d830f160c921192a30"`); + await queryRunner.query(`DROP TABLE "instance_bans"`); + } + +} From 8a785da5e4430dc51a5af7b40a2a6fe2f495d778 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 12 Dec 2025 23:58:59 +0100 Subject: [PATCH 08/34] Fix CI tests --- flake.nix | 55 +++++++++++++++++--------------- nix/tests/test-bundle-starts.nix | 9 +++++- scripts/test.js | 17 ++++++++-- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/flake.nix b/flake.nix index dd947375a..c5affe663 100644 --- a/flake.nix +++ b/flake.nix @@ -54,36 +54,39 @@ python3 ]; installPhase = - let - revsFile = pkgs.writeText "spacebar-server-rev.json" (builtins.toJSON { - rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev; - shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev; - lastModified = self.sourceInfo.lastModified; - }); - in '' - runHook preInstall - # set -x + let + revsFile = pkgs.writeText "spacebar-server-rev.json" ( + builtins.toJSON { + rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev; + shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev; + lastModified = self.sourceInfo.lastModified; + } + ); + in + '' + runHook preInstall + # set -x - # remove packages not needed for production, or at least try to... - npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}" - ${./nix/trimNodeModules.sh} + # remove packages not needed for production, or at least try to... + npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}" + ${./nix/trimNodeModules.sh} - # Copy outputs - echo "Installing package into $out" - mkdir -p $out - cp -r assets dist node_modules package.json $out/ - cp ${revsFile} $out/.rev + # Copy outputs + echo "Installing package into $out" + mkdir -p $out + cp -r assets dist node_modules package.json $out/ + cp ${revsFile} $out/.rev - # Create wrappers for start scripts - echo "Creating wrappers for start scripts" - for i in dist/**/start.js - do - makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i - done + # Create wrappers for start scripts + echo "Creating wrappers for start scripts" + for i in dist/**/start.js + do + makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i + done - # set +x - runHook postInstall - ''; + # set +x + runHook postInstall + ''; passthru.tests = pkgs.testers.runNixOSTest (import ./nix/tests/test-bundle-starts.nix self); }; diff --git a/nix/tests/test-bundle-starts.nix b/nix/tests/test-bundle-starts.nix index 4378369cc..14108a411 100644 --- a/nix/tests/test-bundle-starts.nix +++ b/nix/tests/test-bundle-starts.nix @@ -7,7 +7,14 @@ self: nodes.machine = { imports = [ self.nixosModules.default ]; - services.spacebarchat-server.enable = true; + services.spacebarchat-server = { + enable = true; + settings = { + api = { endpointPublic = "http://localhost:3001/api/v9/"; }; + cdn = { endpointPublic = "http://localhost:3001/"; endpointPrivate = "http://localhost:3001/"; }; + gateway = { endpointPublic = "ws://localhost:3001/"; }; + }; + }; }; testScript = '' diff --git a/scripts/test.js b/scripts/test.js index 69e9fdd64..c79e336ff 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -23,10 +23,21 @@ const { spawn } = require("child_process"); const path = require("path"); +const fs = require("fs"); -const server = spawn("node", [ - path.join(__dirname, "..", "dist", "bundle", "start.js"), -]); +const cfgFile = path.join(__dirname, "test_config.json"); +process.env.CONFIG_PATH = cfgFile; + +fs.writeFileSync( + cfgFile, + JSON.stringify({ + api: { endpointPublic: "http://localhost:3001/api/v9/" }, + cdn: { endpointPublic: "http://localhost:3001/", endpointPrivate: "http://localhost:3001/" }, + gateway: { endpointPublic: "ws://localhost:3001/" }, + }), +); + +const server = spawn("node", [path.join(__dirname, "..", "dist", "bundle", "start.js")]); server.stdout.on("data", (data) => { process.stdout.write(data); From dae89fd0afe54a9ab05e2f5484a40ae9101b1a82 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:01:32 +0100 Subject: [PATCH 09/34] Instance ban stuff --- .idea/workspace.xml | 17 +++-- scripts/util/getRouteDescriptions.js | 2 +- src/api/middlewares/Authentication.ts | 16 ++++- src/api/routes/auth/reset.ts | 2 + src/api/routes/auth/verify/index.ts | 5 +- .../config/types/SecurityConfiguration.ts | 2 +- src/util/entities/InstanceBan.ts | 56 +++++++++++++++- src/util/util/Token.ts | 66 ++++++------------- 8 files changed, 109 insertions(+), 57 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index c52fdde44..dfbf2c4a5 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -55,9 +55,10 @@ "Node.js.Server.ts.executor": "Debug", "RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.git.unshallow": "true", + "git-widget-placeholder": "master", "javascript.nodejs.core.library.configured.version": "24.8.0", "javascript.nodejs.core.library.typings.version": "24.7.0", - "last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/util/migration/postgres", + "last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/schemas/api/users", "node.js.detected.package.eslint": "true", "node.js.detected.package.standard": "true", "node.js.selected.package.eslint": "(autodetect)", @@ -71,7 +72,7 @@ "npm.build.executor": "Run", "npm.start.executor": "Debug", "prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier", - "settings.editor.selected.configurable": "preferences.sourceCode", + "settings.editor.selected.configurable": "com.github.copilot.settings.customInstructions.CopilotInstructionsConfigurable", "ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib" }, "keyToStringList": { @@ -87,11 +88,11 @@ }]]> - - - - + + + + @@ -161,7 +162,9 @@ - + + + diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js index 9f329f323..0372aae7e 100644 --- a/scripts/util/getRouteDescriptions.js +++ b/scripts/util/getRouteDescriptions.js @@ -49,7 +49,7 @@ function proxy(file, apiMethod, apiPathPrefix, apiPath, ...args) { const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true); if (!opts) return console.error( - ` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src/")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`, + ` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`, ); console.log(`${colorizeMethod(apiMethod).padStart("DELETE".length + 10)} ${formatPath(apiPathPrefix + apiPath)}`); diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 4bb809a67..5d239a17e 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -70,6 +70,7 @@ declare global { user_bot: boolean; token: { id: string; iat: number }; rights: Rights; + fingerprint?: string; } } } @@ -77,6 +78,15 @@ declare global { export async function Authentication(req: Request, res: Response, next: NextFunction) { if (req.method === "OPTIONS") return res.sendStatus(204); const url = req.url.replace(API_PREFIX, ""); + + if (req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) + req.fingerprint = req.headers.cookie + .split("; ") + .find((x) => x.startsWith("__sb_sessid="))! + .split("=")[1]; + // for some reason we need to require here, else the openapi generator fails with "route is not a function" + else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None`); + if ( NO_AUTHORIZATION_ROUTES.some((x) => { if (typeof x !== "string") { @@ -102,10 +112,14 @@ export async function Authentication(req: Request, res: Response, next: NextFunc }) ) return next(); + if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401)); try { - const { decoded, user } = await checkToken(req.headers.authorization); + const { decoded, user } = await checkToken(req.headers.authorization, { + ipAddress: req.ip, + fingerprint: req.fingerprint, + }); req.token = decoded; req.user_id = decoded.id; diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts index b14eb3791..5cb5690d3 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -51,6 +51,8 @@ router.post( try { const userTokenData = await checkToken(token, { select: ["email"], + fingerprint: req.fingerprint, + ipAddress: req.ip }); user = userTokenData.user; } catch { diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index d8e343cd2..8c636196e 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -75,7 +75,10 @@ router.post( let user; try { - const userTokenData = await checkToken(token); + const userTokenData = await checkToken(token, { + fingerprint: req.fingerprint, + ipAddress: req.ip + }); user = userTokenData.user; } catch { throw FieldErrors({ diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index ae1c6cb87..9fbf5d218 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -24,7 +24,7 @@ export class SecurityConfiguration { twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration(); autoUpdate: boolean | number = true; requestSignature: string = crypto.randomBytes(32).toString("base64"); - jwtSecret: string = crypto.randomBytes(256).toString("base64"); + jwtSecret: string = crypto.randomBytes(256).toString("base64"); // deprecated // header to get the real user ip address // X-Forwarded-For for nginx/reverse proxies // CF-Connecting-IP for cloudflare diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts index 6f92e2ecc..08100a9fb 100644 --- a/src/util/entities/InstanceBan.ts +++ b/src/util/entities/InstanceBan.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; +import { Column, CreateDateColumn, Entity, FindOptionsWhere, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Team } from "./Team"; import { User } from "./User"; @@ -44,6 +44,9 @@ export class InstanceBan extends BaseClass { // chain of trust type tracking + @Column({ default: false }) + is_allowlisted: boolean = false; + @Column({ default: false }) is_from_other_instance_ban: boolean = false; @@ -54,4 +57,55 @@ export class InstanceBan extends BaseClass { @JoinColumn({ name: "origin_instance_ban_id" }) @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" }) origin_instance_ban?: InstanceBan; + + static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) { + const optionalChecks: FindOptionsWhere[] = [{ user_id: opts.userId }]; + if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress }); + if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint }); + const instanceBans = await InstanceBan.find({ where: optionalChecks }); + + const banReasons = []; + for (const ban of instanceBans) { + if (ban.is_allowlisted) continue; + if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint"); + if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress"); + if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId"); + } + + const banViralityPromises: Promise[] = []; + if (opts.propagateBan && banReasons.length > 0) { + if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + ip_address: opts.ipAddress, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + fingerprint: opts.fingerprint, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + } + + await Promise.all(banViralityPromises); + return banReasons; + } } diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 576349538..db2a313b9 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -18,15 +18,12 @@ import jwt, { VerifyOptions } from "jsonwebtoken"; import { Config } from "./Config"; -import { User } from "../entities"; +import { InstanceBan, User } from "../entities"; import crypto from "node:crypto"; import fs from "fs/promises"; import { existsSync } from "fs"; // TODO: dont use deprecated APIs lol -import { - FindOptionsRelationByString, - FindOptionsSelectByString, -} from "typeorm"; +import { FindManyOptions, FindOptions, FindOptionsRelationByString, FindOptionsSelect, FindOptionsSelectByString, FindOptionsWhere } from "typeorm"; import * as console from "node:console"; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; @@ -37,7 +34,7 @@ export type UserTokenData = { }; function logAuth(text: string) { - if(process.env.LOG_AUTH !== "true") return; + if (process.env.LOG_AUTH !== "true") return; console.log(`[AUTH] ${text}`); } @@ -51,6 +48,8 @@ export const checkToken = ( opts?: { select?: FindOptionsSelectByString; relations?: FindOptionsRelationByString; + ipAddress?: string; + fingerprint?: string; }, ): Promise => new Promise((resolve, reject) => { @@ -66,15 +65,7 @@ export const checkToken = ( const user = await User.findOne({ where: { id: decoded.id }, - select: [ - ...(opts?.select || []), - "id", - "bot", - "disabled", - "deleted", - "rights", - "data", - ], + select: [...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"], relations: opts?.relations, }); @@ -84,10 +75,7 @@ export const checkToken = ( } // 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) - ) { + if (decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0)) { logAuth("validateUser rejected: Token not yet valid"); return rejectAndLog(reject, "Invalid Token"); } @@ -96,11 +84,18 @@ export const checkToken = ( logAuth("validateUser rejected: User disabled"); return rejectAndLog(reject, "User disabled"); } + if (user.deleted) { logAuth("validateUser rejected: User deleted"); return rejectAndLog(reject, "User not found"); } + const banReasons = await InstanceBan.findInstanceBans({ userId: user.id, ipAddress: opts?.ipAddress, fingerprint: opts?.fingerprint, propagateBan: true }); + if (banReasons.length > 0) { + logAuth("validateUser rejected: User banned for reasons: " + banReasons.join(", ")); + return rejectAndLog(reject, "Invalid Token"); + } + logAuth("validateUser success: " + JSON.stringify({ decoded, user })); return resolve({ decoded, user }); }; @@ -110,20 +105,10 @@ export const checkToken = ( logAuth("Decoded token: " + JSON.stringify(dec)); if (dec.header.alg == "HS256") { - jwt.verify( - token, - Config.get().security.jwtSecret, - JWTOptions, - validateUser, - ); + jwt.verify(token, Config.get().security.jwtSecret, JWTOptions, validateUser); } else if (dec.header.alg == "ES512") { loadOrGenerateKeypair().then((keyPair) => { - jwt.verify( - token, - keyPair.publicKey, - { algorithms: ["ES512"] }, - validateUser, - ); + jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser); }); } else return reject("Invalid token algorithm"); }); @@ -152,7 +137,7 @@ let cachedKeypair: { privateKey: crypto.KeyObject; publicKey: crypto.KeyObject; fingerprint: string; -} +}; // Get ECDSA keypair from file or generate it export async function loadOrGenerateKeypair() { @@ -176,10 +161,7 @@ export async function loadOrGenerateKeypair() { let publicKey: crypto.KeyObject; if (existsSync("jwt.key") && existsSync("jwt.key.pub")) { - const [loadedPrivateKey, loadedPublicKey] = await Promise.all([ - fs.readFile("jwt.key"), - fs.readFile("jwt.key.pub"), - ]); + const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]); privateKey = crypto.createPrivateKey(loadedPrivateKey); publicKey = crypto.createPublicKey(loadedPublicKey); @@ -192,14 +174,8 @@ export async function loadOrGenerateKeypair() { publicKey = res.publicKey; await Promise.all([ - fs.writeFile( - "jwt.key", - privateKey.export({ format: "pem", type: "sec1" }), - ), - fs.writeFile( - "jwt.key.pub", - publicKey.export({ format: "pem", type: "spki" }), - ), + fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })), + fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })), ]); } @@ -209,5 +185,5 @@ export async function loadOrGenerateKeypair() { .digest("hex"); lastFsCheck = Date.now(); - return cachedKeypair = { privateKey, publicKey, fingerprint }; + return (cachedKeypair = { privateKey, publicKey, fingerprint }); } From 3f5e25dd3fe94e17dd01c9d3c93fe9143a4ba194 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:04:24 +0100 Subject: [PATCH 10/34] Add missing migration --- .../postgres/1765587835846-InstanceBanAllowlist.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts diff --git a/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts b/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts new file mode 100644 index 000000000..fd8da01ad --- /dev/null +++ b/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InstanceBanAllowlist1765587835846 implements MigrationInterface { + name = 'InstanceBanAllowlist1765587835846' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance_bans" ADD "is_allowlisted" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance_bans" DROP COLUMN "is_allowlisted"`); + } + +} From d41015862c27adbdd049aebaca59555205afb3e1 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:07:35 +0100 Subject: [PATCH 11/34] Fix typo in getDmChannels helper --- src/util/entities/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 18d0f805f..9ef531412 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -364,7 +364,7 @@ export class User extends BaseClass { const qry = await Channel.getRepository() .createQueryBuilder() .leftJoinAndSelect("Channel.recipients", "rcp") - .where("Channel.type = :type", { types: ChannelType.DM }) + .where("Channel.type = :type", { type: ChannelType.DM }) .andWhere("rcp.user_id = :user_id", { user_id: this.id }) .groupBy("Channel.id") .having("COUNT(rcp.user_id) = 2") From a834184690f6b2343cd7f73bc51b87a0e3469a59 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:10:23 +0100 Subject: [PATCH 12/34] Fix missing group by? --- src/util/entities/User.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 9ef531412..7b78b428a 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -345,6 +345,7 @@ export class User extends BaseClass { .where("Channel.type = :type", { type: ChannelType.DM }) .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] }) .groupBy("Channel.id") + .addGroupBy("rcp.user_id") .having("COUNT(rcp.user_id) = 2") .getMany(); @@ -367,6 +368,7 @@ export class User extends BaseClass { .where("Channel.type = :type", { type: ChannelType.DM }) .andWhere("rcp.user_id = :user_id", { user_id: this.id }) .groupBy("Channel.id") + .addGroupBy("rcp.user_id") .having("COUNT(rcp.user_id) = 2") .getMany(); From e6d22ccc47d6fb5a28dd8e295aa46557f2cc88ad Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:24:22 +0100 Subject: [PATCH 13/34] Maybe?> --- src/util/entities/User.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 7b78b428a..375aaba4c 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -345,7 +345,6 @@ export class User extends BaseClass { .where("Channel.type = :type", { type: ChannelType.DM }) .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] }) .groupBy("Channel.id") - .addGroupBy("rcp.user_id") .having("COUNT(rcp.user_id) = 2") .getMany(); @@ -363,13 +362,13 @@ export class User extends BaseClass { async getDmChannels() { const qry = await Channel.getRepository() - .createQueryBuilder() - .leftJoinAndSelect("Channel.recipients", "rcp") - .where("Channel.type = :type", { type: ChannelType.DM }) + .createQueryBuilder("channel") + .leftJoinAndSelect("channel.recipients", "rcp") + .where("channel.type = :type", { type: ChannelType.DM }) .andWhere("rcp.user_id = :user_id", { user_id: this.id }) - .groupBy("Channel.id") - .addGroupBy("rcp.user_id") - .having("COUNT(rcp.user_id) = 2") + .groupBy("channel.id") + .addGroupBy("rcp.id") + .having("COUNT(rcp.id) = 2") .getMany(); return qry; From 5de70ca2c8a510476b4c07bd28d03ad3b37e8c40 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:44:12 +0100 Subject: [PATCH 14/34] Log elapsed time for user delete --- src/api/routes/users/#user_id/delete.ts | 18 +++++++++++++++++- src/util/util/ElapsedTime.ts | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index 277aeb386..7c39e2b73 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -17,7 +17,21 @@ */ import { route } from "@spacebar/api"; -import { Channel, ChannelDeleteEvent, ChannelRecipientRemoveEvent, emitEvent, Emoji, Guild, InstanceBan, Member, Recipient, Sticker, User, UserDeleteEvent } from "@spacebar/util"; +import { + Channel, + ChannelDeleteEvent, + ChannelRecipientRemoveEvent, + emitEvent, + Emoji, + Guild, + InstanceBan, + Member, + Recipient, + Sticker, + Stopwatch, + User, + UserDeleteEvent, +} from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas"; import { Not } from "typeorm"; @@ -40,6 +54,7 @@ router.post( }, }), async (req: Request, res: Response) => { + const sw = Stopwatch.startNew(); const body = req.body as InstanceUserDeleteSchema | undefined; const user = await User.findOneOrFail({ where: { id: req.params.user_id }, @@ -170,6 +185,7 @@ router.post( data: { user_id: req.params.user_id }, } as UserDeleteEvent); + console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()} ms`); res.sendStatus(204); }, ); diff --git a/src/util/util/ElapsedTime.ts b/src/util/util/ElapsedTime.ts index 518025c61..607b00c5f 100644 --- a/src/util/util/ElapsedTime.ts +++ b/src/util/util/ElapsedTime.ts @@ -68,4 +68,17 @@ export class ElapsedTime { get days(): number { return this.totalDays; } + + toString(): string { + // Format: "DD.HH:MM:SS.mmmuuuNNN", with days being optional + const daysPart = this.days > 0 ? `${this.days}.` : ""; + const hoursPart = this.hours.toString().padStart(2, "0"); + const minutesPart = this.minutes.toString().padStart(2, "0"); + const secondsPart = this.seconds.toString().padStart(2, "0"); + const millisecondsPart = this.milliseconds.toString().padStart(3, "0"); + const microsecondsPart = this.microseconds.toString().padStart(3, "0"); + const nanosecondsPart = this.nanoseconds.toString().padStart(3, "0"); + + return `${daysPart}${hoursPart}:${minutesPart}:${secondsPart}.${millisecondsPart}${microsecondsPart}${nanosecondsPart}`; + } } \ No newline at end of file From 1a41d194bfe9a486ac8237ad6a2e93607e22fd68 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:47:49 +0100 Subject: [PATCH 15/34] Fix formatting oopsie --- src/util/util/ElapsedTime.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/util/util/ElapsedTime.ts b/src/util/util/ElapsedTime.ts index 607b00c5f..08f246a1a 100644 --- a/src/util/util/ElapsedTime.ts +++ b/src/util/util/ElapsedTime.ts @@ -71,13 +71,13 @@ export class ElapsedTime { toString(): string { // Format: "DD.HH:MM:SS.mmmuuuNNN", with days being optional - const daysPart = this.days > 0 ? `${this.days}.` : ""; - const hoursPart = this.hours.toString().padStart(2, "0"); - const minutesPart = this.minutes.toString().padStart(2, "0"); - const secondsPart = this.seconds.toString().padStart(2, "0"); - const millisecondsPart = this.milliseconds.toString().padStart(3, "0"); - const microsecondsPart = this.microseconds.toString().padStart(3, "0"); - const nanosecondsPart = this.nanoseconds.toString().padStart(3, "0"); + const daysPart = Math.floor(this.days) > 0 ? `${Math.floor(this.days)}.` : ""; + const hoursPart = Math.floor(this.hours).toString().padStart(2, "0"); + const minutesPart = Math.floor(this.minutes).toString().padStart(2, "0"); + const secondsPart = Math.floor(this.seconds).toString().padStart(2, "0"); + const millisecondsPart = Math.floor(this.milliseconds).toString().padStart(3, "0"); + const microsecondsPart = Math.floor(this.microseconds).toString().padStart(3, "0"); + const nanosecondsPart = Math.floor(this.nanoseconds).toString().padStart(3, "0"); return `${daysPart}${hoursPart}:${minutesPart}:${secondsPart}.${millisecondsPart}${microsecondsPart}${nanosecondsPart}`; } From ca066536067d40305ffc4ba6b4d5418c2996f9bd Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:48:50 +0100 Subject: [PATCH 16/34] not milliseconds lol --- src/api/routes/users/#user_id/delete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index 7c39e2b73..eb20dba5f 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -185,7 +185,7 @@ router.post( data: { user_id: req.params.user_id }, } as UserDeleteEvent); - console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()} ms`); + console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()}`); res.sendStatus(204); }, ); From 91c112c72f8455573d8dd1a74b85d782c9683ff5 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:51:25 +0100 Subject: [PATCH 17/34] Delete user settings protos on account deletion --- src/api/routes/users/#user_id/delete.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index eb20dba5f..a0b93cc02 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -31,6 +31,7 @@ import { Stopwatch, User, UserDeleteEvent, + UserSettingsProtos, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas"; @@ -176,7 +177,9 @@ router.post( ); const members = await Member.find({ where: { id: req.params.user_id } }); - await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id)), User.delete({ id: req.params.user_id })]); + await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id))]); + await UserSettingsProtos.delete({ user_id: req.params.user_id }); + await User.delete({ id: req.params.user_id }); // TODO: respect intents as USER_DELETE has potential to cause privacy issues await emitEvent({ From 6683dbac7ba84cf5299b727443c608ac356e4fcd Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 02:55:25 +0100 Subject: [PATCH 18/34] Prevent duplicate instance ban entries --- src/api/routes/users/#user_id/delete.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index a0b93cc02..50494a238 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -62,7 +62,8 @@ router.post( select: [...PrivateUserProjection, "data"], }); - await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "" }).save(); + if (!(await InstanceBan.findOne({ where: { user_id: user.id } }))) + await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "" }).save(); // prevent bugginess with clients - delete all DMs, only having half of the conversation is quite useless anyhow const dmChannels = await user.getDmChannels(); From 91cf11ac6fb29bf97d6d14372d851dd4994a94b6 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 20:16:42 +0100 Subject: [PATCH 19/34] Try adjusting CORS rules to make cookies work? --- src/api/middlewares/Authentication.ts | 2 +- src/api/middlewares/CORS.ts | 14 +++++--------- src/gateway/Server.ts | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 5d239a17e..2dd992ee3 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -85,7 +85,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc .find((x) => x.startsWith("__sb_sessid="))! .split("=")[1]; // for some reason we need to require here, else the openapi generator fails with "route is not a function" - else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None`); + else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`); if ( NO_AUTHORIZATION_ROUTES.some((x) => { diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts index ca6abb82d..84c49a8bd 100644 --- a/src/api/middlewares/CORS.ts +++ b/src/api/middlewares/CORS.ts @@ -21,20 +21,16 @@ import { NextFunction, Request, Response } from "express"; // TODO: config settings export function CORS(req: Request, res: Response, next: NextFunction) { - res.set("Access-Control-Allow-Origin", "*"); + res.set("Access-Control-Allow-Credentials", "true"); + res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*"); + res.set("Access-Control-Allow-Methods", req.header("Access-Control-Request-Methods") || "*"); + res.set("Access-Control-Allow-Origin", req.header("Origin") ?? "*"); + res.set("Access-Control-Max-Age", "5"); // dont make it too long so we can change it dynamically // TODO: use better CSP res.set( "Content-security-policy", "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';", ); - res.set( - "Access-Control-Allow-Headers", - req.header("Access-Control-Request-Headers") || "*", - ); - res.set( - "Access-Control-Allow-Methods", - req.header("Access-Control-Request-Methods") || "*", - ); if (req.method === "OPTIONS") { res.status(204).end(); diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts index 5d7f8d87a..2ad3e4136 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts @@ -52,7 +52,7 @@ export class Server { else { this.server = http.createServer(function (req, res) { if(!req.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) { - res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None`); + res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`); } res.writeHead(200).end("Online"); From 1085754f52321b452cecb033903dca39cd0da576 Mon Sep 17 00:00:00 2001 From: dank074 Date: Sat, 13 Dec 2025 13:43:02 -0600 Subject: [PATCH 20/34] try to handle race condition again --- src/util/util/Event.ts | 20 +++++++++++++------- src/util/util/RabbitMQ.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts index ce5c25d25..db4e8066e 100644 --- a/src/util/util/Event.ts +++ b/src/util/util/Event.ts @@ -29,13 +29,18 @@ export async function emitEvent(payload: Omit) { if (RabbitMQ.connection) { const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission - await RabbitMQ.channel?.assertExchange(id, "fanout", { - durable: false, - }); + const channel = await RabbitMQ.getSafeChannel(); + try { + await channel.assertExchange(id, "fanout", { + durable: false, + }); - // assertQueue isn't needed, because a queue will automatically created if it doesn't exist - const successful = RabbitMQ.channel?.publish(id, "", Buffer.from(`${data}`), { type: payload.event }); - if (!successful) throw new Error("failed to send event"); + // assertQueue isn't needed, because a queue will automatically created if it doesn't exist + const successful = channel.publish(id, "", Buffer.from(`${data}`), { type: payload.event }); + if (!successful) throw new Error("failed to send event"); + } catch (e) { + console.log("[RabbitMQ] ", e); + } } else if (process.env.EVENT_TRANSMISSION === "process") { process.send?.({ type: "event", event: payload, id } as ProcessEvent); } else { @@ -72,7 +77,8 @@ export interface ProcessEvent { export async function listenEvent(event: string, callback: (event: EventOpts) => unknown, opts?: ListenEventOpts): Promise<() => Promise> { if (RabbitMQ.connection) { - const channel = opts?.channel || RabbitMQ.channel; + const rabbitMQChannel = await RabbitMQ.getSafeChannel(); + const channel = opts?.channel || rabbitMQChannel; if (!channel) throw new Error("[Events] An event was sent without an associated channel"); return await rabbitListen(channel, event, callback, { acknowledge: opts?.acknowledge, diff --git a/src/util/util/RabbitMQ.ts b/src/util/util/RabbitMQ.ts index 89e8e140c..f11f701fb 100644 --- a/src/util/util/RabbitMQ.ts +++ b/src/util/util/RabbitMQ.ts @@ -23,6 +23,7 @@ export const RabbitMQ: { connection: ChannelModel | null; channel: Channel | null; init: () => Promise; + getSafeChannel: () => Promise; } = { connection: null, channel: null, @@ -46,6 +47,13 @@ export const RabbitMQ: { // will be a pain since we will have to reconstruct entire state }); + await this.getSafeChannel(); + }, + getSafeChannel: async function () { + if (!this.connection) return Promise.reject(); + + if (this.channel) return this.channel; + this.channel = await this.connection.createChannel(); console.log(`[RabbitMQ] channel created`); @@ -53,5 +61,12 @@ export const RabbitMQ: { this.channel.on("error", (err) => { console.error("[RabbitMQ] Channel Error:", err); }); + + this.channel.on("close", () => { + console.log("[RabbitMQ] channel closed"); + this.channel = null; + }); + + return this.channel; }, }; From c95dd395c94d0f38108957e9436860a12d110391 Mon Sep 17 00:00:00 2001 From: dank074 Date: Sat, 13 Dec 2025 13:53:03 -0600 Subject: [PATCH 21/34] comment --- src/util/util/Event.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts index db4e8066e..6f2b0d9a9 100644 --- a/src/util/util/Event.ts +++ b/src/util/util/Event.ts @@ -39,6 +39,7 @@ export async function emitEvent(payload: Omit) { const successful = channel.publish(id, "", Buffer.from(`${data}`), { type: payload.event }); if (!successful) throw new Error("failed to send event"); } catch (e) { + // todo: should we retry publishng the event? console.log("[RabbitMQ] ", e); } } else if (process.env.EVENT_TRANSMISSION === "process") { From fad824c935fd21e1144b0e0df831d3fcbc656459 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 22:57:05 +0100 Subject: [PATCH 22/34] No more sync:db on initial setup >:C >:C >:C --- src/util/migration/postgres-initial.ts | 727 ++++++++++++++++++ .../1696420827239-guildChannelOrdering.ts | 26 +- .../1713116476900-messageFlagsNotNull.ts | 14 +- .../postgres/1720628601997-badges.ts | 1 + .../postgres/1752321571508-RoleColors.ts | 2 + .../1752342900886-RoleColorsSolidColor.ts | 15 - .../1752383879533-message_pinned_at.ts | 6 +- ...delete-bot-users-without-an-application.ts | 1 + ...2611552514-fix-gif-stickers-format_type.ts | 1 + src/util/util/Database.ts | 37 +- 10 files changed, 779 insertions(+), 51 deletions(-) create mode 100644 src/util/migration/postgres-initial.ts delete mode 100644 src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts diff --git a/src/util/migration/postgres-initial.ts b/src/util/migration/postgres-initial.ts new file mode 100644 index 000000000..769794175 --- /dev/null +++ b/src/util/migration/postgres-initial.ts @@ -0,0 +1,727 @@ +// @prettier-ignore +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { MigrationInterface, QueryRunner } from "typeorm"; +export class initial0 implements MigrationInterface { + name = "initial0"; + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE public.applications ( + id character varying NOT NULL, + name character varying NOT NULL, + icon character varying, + description character varying, + summary character varying, + type text, + hook boolean NOT NULL, + bot_public boolean NOT NULL, + bot_require_code_grant boolean NOT NULL, + verify_key character varying NOT NULL, + flags integer NOT NULL, + redirect_uris text, + rpc_application_state integer, + store_application_state integer, + verification_state integer, + interactions_endpoint_url character varying, + integration_public boolean, + integration_require_code_grant boolean, + discoverability_state integer, + discovery_eligibility_flags integer, + tags text, + cover_image character varying, + install_params text, + terms_of_service_url character varying, + privacy_policy_url character varying, + owner_id character varying, + bot_user_id character varying, + team_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.attachments ( + id character varying NOT NULL, + filename character varying NOT NULL, + size integer NOT NULL, + url character varying NOT NULL, + proxy_url character varying NOT NULL, + height integer, + width integer, + content_type character varying, + message_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.audit_logs ( + id character varying NOT NULL, + user_id character varying, + action_type integer NOT NULL, + options text, + changes text NOT NULL, + reason character varying, + target_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.backup_codes ( + id character varying NOT NULL, + code character varying NOT NULL, + consumed boolean NOT NULL, + expired boolean NOT NULL, + user_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.bans ( + id character varying NOT NULL, + user_id character varying, + guild_id character varying, + executor_id character varying, + ip character varying NOT NULL, + reason character varying + );`); + + await queryRunner.query(`CREATE TABLE public.categories ( + id integer NOT NULL, + name character varying, + localizations text NOT NULL, + is_primary boolean + );`); + + await queryRunner.query(`CREATE TABLE public.channels ( + id character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + name character varying, + icon text, + type integer NOT NULL, + last_message_id character varying, + guild_id character varying, + parent_id character varying, + owner_id character varying, + last_pin_timestamp integer, + default_auto_archive_duration integer, + permission_overwrites text, + video_quality_mode integer, + bitrate integer, + user_limit integer, + nsfw boolean NOT NULL, + rate_limit_per_user integer, + topic character varying, + retention_policy_id character varying, + flags integer NOT NULL, + default_thread_rate_limit_per_user integer NOT NULL, + position integer NOT NULL DEFAULT 0 + );`); + + await queryRunner.query(`CREATE TABLE public.client_release ( + id character varying NOT NULL, + name character varying NOT NULL, + pub_date timestamp without time zone NOT NULL, + url character varying NOT NULL, + platform character varying NOT NULL, + enabled boolean NOT NULL, + notes character varying + );`); + + await queryRunner.query(`CREATE TABLE public.config ( + key character varying NOT NULL, + value text + );`); + + await queryRunner.query(`CREATE TABLE public.connected_accounts ( + id character varying NOT NULL, + external_id character varying NOT NULL, + user_id character varying, + friend_sync boolean NOT NULL, + name character varying NOT NULL, + revoked boolean NOT NULL, + show_activity integer NOT NULL, + type character varying NOT NULL, + verified boolean NOT NULL, + visibility integer NOT NULL, + integrations text NOT NULL, + metadata text, + metadata_visibility integer NOT NULL, + two_way_link boolean NOT NULL, + token_data text + );`); + + await queryRunner.query(`CREATE TABLE public.connection_config ( + key character varying NOT NULL, + value text + );`); + + await queryRunner.query(`CREATE TABLE public.embed_cache ( + id character varying NOT NULL, + url character varying NOT NULL, + embed text NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.emojis ( + id character varying NOT NULL, + animated boolean NOT NULL, + available boolean NOT NULL, + guild_id character varying NOT NULL, + user_id character varying, + managed boolean NOT NULL, + name character varying NOT NULL, + require_colons boolean NOT NULL, + roles text NOT NULL, + groups text + );`); + + await queryRunner.query(`CREATE TABLE public.guilds ( + id character varying NOT NULL, + afk_channel_id character varying, + afk_timeout integer, + banner character varying, + default_message_notifications integer, + description character varying, + discovery_splash character varying, + explicit_content_filter integer, + features text NOT NULL, + primary_category_id character varying, + icon character varying, + large boolean NOT NULL, + max_members integer, + max_presences integer, + max_video_channel_users integer, + member_count integer, + presence_count integer, + template_id character varying, + mfa_level integer, + name character varying NOT NULL, + owner_id character varying, + preferred_locale character varying, + premium_subscription_count integer, + premium_tier integer NOT NULL, + public_updates_channel_id character varying, + rules_channel_id character varying, + region character varying, + splash character varying, + system_channel_id character varying, + system_channel_flags integer, + unavailable boolean NOT NULL, + verification_level integer, + welcome_screen text NOT NULL, + widget_channel_id character varying, + widget_enabled boolean NOT NULL, + nsfw_level integer, + nsfw boolean NOT NULL, + parent character varying, + premium_progress_bar_enabled boolean + );`); + + await queryRunner.query(`CREATE TABLE public.invites ( + code character varying NOT NULL, + temporary boolean NOT NULL, + uses integer NOT NULL, + max_uses integer NOT NULL, + max_age integer NOT NULL, + created_at timestamp without time zone NOT NULL, + expires_at timestamp without time zone, + guild_id character varying, + channel_id character varying, + inviter_id character varying, + target_user_id character varying, + target_user_type integer, + vanity_url boolean, + flags integer NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.member_roles ( + index integer NOT NULL, + role_id character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.members ( + index integer NOT NULL, + id character varying NOT NULL, + guild_id character varying NOT NULL, + nick character varying, + joined_at timestamp without time zone NOT NULL, + premium_since bigint, + deaf boolean NOT NULL, + mute boolean NOT NULL, + pending boolean NOT NULL, + settings text NOT NULL, + last_message_id character varying, + joined_by character varying, + avatar character varying, + banner character varying, + bio character varying NOT NULL, + theme_colors text, + pronouns character varying, + communication_disabled_until timestamp without time zone + );`); + + await queryRunner.query(`CREATE SEQUENCE public.members_index_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1;`); + + await queryRunner.query(`ALTER SEQUENCE public.members_index_seq OWNED BY public.members.index;`); + await queryRunner.query(`CREATE TABLE public.message_channel_mentions ( + "messagesId" character varying NOT NULL, + "channelsId" character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.message_role_mentions ( + "messagesId" character varying NOT NULL, + "rolesId" character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.message_stickers ( + "messagesId" character varying NOT NULL, + "stickersId" character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.message_user_mentions ( + "messagesId" character varying NOT NULL, + "usersId" character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.messages ( + id character varying NOT NULL, + channel_id character varying, + guild_id character varying, + author_id character varying, + member_id character varying, + webhook_id character varying, + application_id character varying, + content character varying, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL, + edited_timestamp timestamp without time zone, + tts boolean, + mention_everyone boolean, + embeds text NOT NULL, + reactions text NOT NULL, + nonce text, + type integer NOT NULL, + activity text, + message_reference text, + interaction text, + components text, + message_reference_id character varying, + pinned boolean, + flags integer + );`); + + await queryRunner.query(`CREATE TABLE public.migrations ( + id integer NOT NULL, + "timestamp" bigint NOT NULL, + name character varying NOT NULL + );`); + + await queryRunner.query(`CREATE SEQUENCE public.migrations_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1;`); + + await queryRunner.query(`ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id;`); + await queryRunner.query(`CREATE TABLE public.notes ( + id character varying NOT NULL, + content character varying NOT NULL, + owner_id character varying, + target_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.rate_limits ( + id character varying NOT NULL, + executor_id character varying NOT NULL, + hits integer NOT NULL, + blocked boolean NOT NULL, + expires_at timestamp without time zone NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.read_states ( + id character varying NOT NULL, + channel_id character varying NOT NULL, + user_id character varying NOT NULL, + last_message_id character varying, + public_ack character varying, + notifications_cursor character varying, + last_pin_timestamp timestamp without time zone, + mention_count integer + );`); + + await queryRunner.query(`CREATE TABLE public.recipients ( + id character varying NOT NULL, + channel_id character varying NOT NULL, + user_id character varying NOT NULL, + closed boolean DEFAULT false NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.relationships ( + id character varying NOT NULL, + from_id character varying NOT NULL, + to_id character varying NOT NULL, + nickname character varying, + type integer NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.roles ( + id character varying NOT NULL, + guild_id character varying NOT NULL, + color integer NOT NULL, + hoist boolean NOT NULL, + managed boolean NOT NULL, + mentionable boolean NOT NULL, + name character varying NOT NULL, + permissions character varying NOT NULL, + "position" integer NOT NULL, + icon character varying, + unicode_emoji character varying, + tags text, + flags integer DEFAULT 0 NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.security_settings ( + id character varying NOT NULL, + guild_id character varying, + channel_id character varying, + encryption_permission_mask integer NOT NULL, + allowed_algorithms text NOT NULL, + current_algorithm character varying NOT NULL, + used_since_message character varying + );`); + + await queryRunner.query(`CREATE TABLE public.sessions ( + id character varying NOT NULL, + user_id character varying, + session_id character varying NOT NULL, + activities text, + client_info text NOT NULL, + status character varying NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.sticker_packs ( + id character varying NOT NULL, + name character varying NOT NULL, + description character varying, + banner_asset_id character varying, + cover_sticker_id character varying, + "coverStickerId" character varying + );`); + + await queryRunner.query(`CREATE TABLE public.stickers ( + id character varying NOT NULL, + name character varying NOT NULL, + description character varying, + available boolean, + tags character varying, + pack_id character varying, + guild_id character varying, + user_id character varying, + type integer NOT NULL, + format_type integer NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.team_members ( + id character varying NOT NULL, + membership_state integer NOT NULL, + permissions text NOT NULL, + team_id character varying, + user_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.teams ( + id character varying NOT NULL, + icon character varying, + name character varying NOT NULL, + owner_user_id character varying + );`); + + await queryRunner.query(`CREATE TABLE public.templates ( + id character varying NOT NULL, + code character varying NOT NULL, + name character varying NOT NULL, + description character varying, + usage_count integer, + creator_id character varying, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + source_guild_id character varying, + serialized_source_guild text NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.user_settings ( + index integer NOT NULL, + afk_timeout integer, + allow_accessibility_detection boolean, + animate_emoji boolean, + animate_stickers integer, + contact_sync_enabled boolean, + convert_emoticons boolean, + custom_status text, + default_guilds_restricted boolean, + detect_platform_accounts boolean, + developer_mode boolean, + disable_games_tab boolean, + enable_tts_command boolean, + explicit_content_filter integer, + friend_source_flags text, + gateway_connected boolean, + gif_auto_play boolean, + guild_folders text, + guild_positions text, + inline_attachment_media boolean, + inline_embed_media boolean, + locale character varying, + message_display_compact boolean, + native_phone_integration_enabled boolean, + render_embeds boolean, + render_reactions boolean, + restricted_guilds text, + show_current_game boolean, + status character varying, + stream_notifications_enabled boolean, + theme character varying, + timezone_offset integer + );`); + + await queryRunner.query(`CREATE SEQUENCE public.user_settings_index_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1;` + ); + + await queryRunner.query(`ALTER SEQUENCE public.user_settings_index_seq OWNED BY public.user_settings.index;`); + await queryRunner.query(`CREATE TABLE public.users ( + id character varying NOT NULL, + username character varying NOT NULL, + discriminator character varying NOT NULL, + avatar character varying, + accent_color integer, + banner character varying, + theme_colors text, + pronouns character varying, + phone character varying, + desktop boolean NOT NULL, + mobile boolean NOT NULL, + premium boolean NOT NULL, + premium_type integer NOT NULL, + bot boolean NOT NULL, + bio character varying NOT NULL, + system boolean NOT NULL, + nsfw_allowed boolean NOT NULL, + mfa_enabled boolean NOT NULL, + totp_secret character varying, + totp_last_ticket character varying, + created_at timestamp without time zone NOT NULL, + premium_since timestamp without time zone, + verified boolean NOT NULL, + disabled boolean NOT NULL, + deleted boolean NOT NULL, + email character varying, + flags bigint NOT NULL, + public_flags bigint NOT NULL, + purchased_flags bigint NOT NULL, + premium_usage_flags integer NOT NULL, + rights bigint NOT NULL, + data text NOT NULL, + fingerprints text NOT NULL, + extended_settings text NOT NULL, + "settingsIndex" integer + );`); + + await queryRunner.query(`CREATE TABLE public.valid_registration_tokens ( + token character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + expires_at timestamp without time zone NOT NULL + );`); + + await queryRunner.query(`CREATE TABLE public.voice_states ( + id character varying NOT NULL, + guild_id character varying, + channel_id character varying, + user_id character varying, + session_id character varying NOT NULL, + token character varying, + deaf boolean NOT NULL, + mute boolean NOT NULL, + self_deaf boolean NOT NULL, + self_mute boolean NOT NULL, + self_stream boolean, + self_video boolean NOT NULL, + suppress boolean NOT NULL, + request_to_speak_timestamp timestamp without time zone + );`); + + await queryRunner.query(`CREATE TABLE public.webhooks ( + id character varying NOT NULL, + type integer NOT NULL, + name character varying, + avatar character varying, + token character varying, + guild_id character varying, + channel_id character varying, + application_id character varying, + user_id character varying, + source_guild_id character varying + );`); + + await queryRunner.query(`ALTER TABLE ONLY public.members ALTER COLUMN index SET DEFAULT nextval('public.members_index_seq'::regclass);`); + await queryRunner.query(`ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass);`); + await queryRunner.query(`ALTER TABLE ONLY public.user_settings ALTER COLUMN index SET DEFAULT nextval('public.user_settings_index_seq'::regclass);`); + await queryRunner.query(`ALTER TABLE ONLY public.embed_cache ADD CONSTRAINT "PK_0abb7581d4efc5a8b1361389c5e" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "PK_18325f38ae6de43878487eff986" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "PK_1bb179d048bbc581caa3b013439" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.categories ADD CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.config ADD CONSTRAINT "PK_26489c99ddbb4c91631ef5cc791" PRIMARY KEY (key);`); + await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "PK_3238ef96f18b355b671619111bc" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "PK_33fd8a248db1cd832baa8aa25bf" PRIMARY KEY (code);`); + await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "PK_34ab957382dbc57e8fb53f1638f" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.rate_limits ADD CONSTRAINT "PK_3b4449f1f5fc167d921ee619f65" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.security_settings ADD CONSTRAINT "PK_4aec436cf81177ae97a1bcec3c7" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.client_release ADD CONSTRAINT "PK_4c4ea258342d2d6ba1be0a71a43" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "PK_515948649ce0bbbe391de702ae5" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "PK_5e1f050bcff31e3084a1d662412" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "PK_70416f1da0be645bb31da01c774" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "PK_74dba92cc300452a6e14b83ed44" PRIMARY KEY ("messagesId", "rolesId");`); + await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "PK_7e5523774a38b08a6236d322403" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "PK_85cb45351497cd9d06a79ced65e" PRIMARY KEY ("messagesId", "channelsId");`); + await queryRunner.query(`ALTER TABLE ONLY public.migrations ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "PK_938c0a27255637bde919591888f" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "PK_951c1d72a0fd1da8760b4a1fd66" PRIMARY KEY (index, role_id);`); + await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "PK_9adb96a675f555c6169bad7ba62" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "PK_9b9b6e245ad47a48dbd7605d4fb" PRIMARY KEY ("messagesId", "usersId");`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "PK_9e8795cfc899ab7bdaa831e8527" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "PK_a27381efea0f876f5d3233af655" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "PK_a4d6f261bffa4615c62d756566a" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "PK_ada09a50c134fad1369b510e3ce" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "PK_af6206538ea96c4e77e9f400c3d" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "PK_b4a6b8c2478e5df990909c6cf6a" PRIMARY KEY (index);`); + await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "PK_ba20e2f5cf487408e08e4dcecaf" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.connection_config ADD CONSTRAINT "PK_bc0554f736ad71dde346549488a" PRIMARY KEY (key);`); + await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "PK_bc603823f3f741359c2339389f9" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "PK_ca3eae89dcf20c9fd95bf7460aa" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "PK_de8fc5a9c364568f294798fe1e9" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.valid_registration_tokens ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY (token);`); + await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "PK_e1dafa4063a5532645cc2810374" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "PK_e6956a804978f01b713b1ed58e2" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "PK_e7e7f2a51bd6d96a9ac2aa560f9" PRIMARY KEY (id);`); + await queryRunner.query(`ALTER TABLE ONLY public.user_settings ADD CONSTRAINT "PK_e81f8bb92802737337d35c00981" PRIMARY KEY (index);`); + await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "PK_ed820c4093d0b8cd1d2bcf66087" PRIMARY KEY ("messagesId", "stickersId");`); + await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "REL_0c14beb78d8c5ccba66072adbc" UNIQUE ("settingsIndex");`); + await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "REL_2ce5a55796fe4c2f77ece57a64" UNIQUE (bot_user_id);`); + await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "UQ_74e6689b9568cc965b8bfc9150b" UNIQUE (owner_id, target_id);`); + await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "UQ_be38737bf339baf63b1daeffb55" UNIQUE (code);`); + await queryRunner.query(`CREATE INDEX "IDX_05535bc695e9f7ee104616459d" ON public.messages USING btree (author_id);`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0abf8b443321bd3cf7f81ee17a" ON public.read_states USING btree (channel_id, user_id);`); + await queryRunner.query(`CREATE INDEX "IDX_29d63eb1a458200851bc37d074" ON public.message_role_mentions USING btree ("rolesId");`); + await queryRunner.query(`CREATE INDEX "IDX_2a27102ecd1d81b4582a436092" ON public.message_channel_mentions USING btree ("messagesId");`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3ed7a60fb7dbe04e1ba9332a8b" ON public.messages USING btree (channel_id, id);`); + await queryRunner.query(`CREATE INDEX "IDX_40bb6f23e7cc133292e92829d2" ON public.message_stickers USING btree ("messagesId");`); + await queryRunner.query(`CREATE INDEX "IDX_5d7ddc8a5f9c167f548625e772" ON public.member_roles USING btree (index);`); + await queryRunner.query(`CREATE INDEX "IDX_86b9109b155eb70c0a2ca3b4b6" ON public.messages USING btree (channel_id);`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0b2ff0a598df0b0d055934a17" ON public.relationships USING btree (from_id, to_id);`); + await queryRunner.query(`CREATE INDEX "IDX_a343387fc560ef378760681c23" ON public.message_user_mentions USING btree ("messagesId");`); + await queryRunner.query(`CREATE INDEX "IDX_a8242cf535337a490b0feaea0b" ON public.message_role_mentions USING btree ("messagesId");`); + await queryRunner.query(`CREATE INDEX "IDX_b831eb18ceebd28976239b1e2f" ON public.message_user_mentions USING btree ("usersId");`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" ON public.members USING btree (id, guild_id);`); + await queryRunner.query(`CREATE INDEX "IDX_bdb8c09e1464cabf62105bf4b9" ON public.message_channel_mentions USING btree ("channelsId");`); + await queryRunner.query(`CREATE INDEX "IDX_e22a70819d07659c7a71c112a1" ON public.message_stickers USING btree ("stickersId");`); + await queryRunner.query(`CREATE INDEX "IDX_e9080e7a7997a0170026d5139c" ON public.member_roles USING btree (role_id);`); + await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_03779ef216d4b0358470d9cb748" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_05535bc695e9f7ee104616459d3" FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_07ad88c86d1f290d46748410d58" FOREIGN KEY (executor_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "FK_085d540d9f418cfbdc7bd55bb19" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "FK_0c14beb78d8c5ccba66072adbc7" FOREIGN KEY ("settingsIndex") REFERENCES public.user_settings(index);`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_0d523f6f997c86e052c49b1455f" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_11a0d394f8fc649c19ce5f16b59" FOREIGN KEY (target_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "FK_13f00abf7cb6096c43ecaf8c108" FOREIGN KEY (owner_user_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_15c35422032e0b22b4ada95f48f" FOREIGN KEY (inviter_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_16aceddd5b89825b8ed6029ad1c" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_193d551d852aca5347ef5c9f205" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_195f92e4dd1254a4e348c043763" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_23e08e5b4481711d573e1abecdc" FOREIGN KEY (target_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_28b53062261b996d9c99fa12404" FOREIGN KEY (id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_29d63eb1a458200851bc37d074b" FOREIGN KEY ("rolesId") REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_2a27102ecd1d81b4582a4360921" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_2ce5a55796fe4c2f77ece57a647" FOREIGN KEY (bot_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_2f18ee1ba667f233ae86c0ea60e" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3274522d14af40540b1a883fc80" FOREIGN KEY (parent_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3873ed438575cce703ecff4fc7b" FOREIGN KEY (owner_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_3a285f4f49c40e0706d3018bc9f" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_3cd01cd3ae7aab010310d96ac8e" FOREIGN KEY (target_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_3f4939aa1461e8af57fea3fb05d" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_40bb6f23e7cc133292e92829d28" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_40da2fca4e0eaf7a23b5bfc5d34" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_445d00eaaea0e60a017a5ed0c11" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "FK_448fafba4355ee1c837bbc865f1" FOREIGN KEY ("coverStickerId") REFERENCES public.stickers(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_487a7af59d189f744fe394368fc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_4b988e0db89d94cebcf07f598cc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_5999e8e449f80a236ff72023559" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_5d3ec1cb962de6488637fd779d6" FOREIGN KEY (application_id) REFERENCES public.applications(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_5d7ddc8a5f9c167f548625e772e" FOREIGN KEY (index) REFERENCES public.members(index) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_5fe1d5f931a67e85039c640001b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_6157e8b6ba4e6e3089616481fe2" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_61a92bb65b302a76d9c1fcd3174" FOREIGN KEY (message_reference_id) REFERENCES public.messages(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "FK_623e10eec51ada466c5038979e3" FOREIGN KEY (message_id) REFERENCES public.messages(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_6a15b051fe5050aa00a4b9ff0f6" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "FK_70066ea80d2f4b871beda32633b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_86b9109b155eb70c0a2ca3b4b6d" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_8d450b016dc8bec35f36729e4b0" FOREIGN KEY (public_updates_channel_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_8f4ee73f2bb2325ff980502e158" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_95828668aa333460582e0ca6396" FOREIGN KEY (rules_channel_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9af4194bab1250b1c584ae4f1d7" FOREIGN KEY (from_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9c7f6b98a9843b76dce1b0c878b" FOREIGN KEY (to_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_9d1d665379eefde7876a17afa99" FOREIGN KEY (widget_channel_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_9d3ab7dd180ebdd245cdb66ecad" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_9f8d389866b40b6657edd026dd4" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_a343387fc560ef378760681c236" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_a36ed02953077f408d0f3ebc424" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_a8242cf535337a490b0feaea0b4" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b0525304f2262b7014245351c76" FOREIGN KEY (member_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b193588441b085352a4c0109423" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_b831eb18ceebd28976239b1e2f8" FOREIGN KEY ("usersId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_bd2726fd31b35443f2245b93ba0" FOREIGN KEY (user_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_bdb8c09e1464cabf62105bf4b9d" FOREIGN KEY ("channelsId") REFERENCES public.channels(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_c253dafe5f3a03ec00cd8fb4581" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_c2bf4967c8c2a6b845dadfbf3d4" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "FK_c32c1ab1c4dc7dcb0278c4b1b8b" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_c3e5305461931763b56aa905f1c" FOREIGN KEY (application_id) REFERENCES public.applications(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_cfc3d3ad260f8121c95b31a1fce" FOREIGN KEY (system_channel_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_d7374b7f8f5fbfdececa4fb62e1" FOREIGN KEY (creator_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_df528cf77e82f8032230e7e37d8" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_e22a70819d07659c7a71c112a1f" FOREIGN KEY ("stickersId") REFERENCES public.stickers(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_e2a2f873a64a5cf62526de42325" FOREIGN KEY (template_id) REFERENCES public.templates(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_e57508958bf92b9d9d25231b5e8" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_e7cfa5cefa6661b3fb8fda8ce69" FOREIGN KEY (pack_id) REFERENCES public.sticker_packs(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_e9080e7a7997a0170026d5139c1" FOREIGN KEY (role_id) REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "FK_f47244225a6a1eac04a3463dd90" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_f591a66b8019d87b0fe6c12dad6" FOREIGN KEY (afk_channel_id) REFERENCES public.channels(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_f83c04bcf1df4e5c0e7a52ed348" FOREIGN KEY (webhook_id) REFERENCES public.webhooks(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_f9e103f8ae67cb1787063597925" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`); + await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_fa7ddd5f9a214e28ce596548421" FOREIGN KEY (user_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_fc1a451727e3643ca572a3bb394" FOREIGN KEY (owner_id) REFERENCES public.users(id);`); + await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_fdad7d5768277e60c40e01cdcea" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error("Can't revert this: just throw away your database lol"); + } +} + diff --git a/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts b/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts index 6fc80ffd6..1623f983e 100644 --- a/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts +++ b/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts @@ -34,7 +34,29 @@ export class guildChannelOrdering1696420827239 implements MigrationInterface { await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`); } - public async down(): Promise { - // don't care actually, sorry. + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE channels ADD position integer NOT NULL DEFAULT 0`); + + const guilds = await queryRunner.query( + `SELECT id, channel_ordering FROM guilds`, + undefined, + true, + ); + + for (const guild of guilds.records) { + const channel_ordering: string[] = JSON.parse(guild.channel_ordering); + + for (let i = 0; i < channel_ordering.length; i++) { + const channel_id = channel_ordering[i]; + await queryRunner.query( + `UPDATE channels SET position = $1 WHERE id = $2`, + [i, channel_id], + ); + } + } + + await queryRunner.query( + `ALTER TABLE guilds DROP COLUMN channel_ordering`, + ); } } diff --git a/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts b/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts index 026b069b9..543a7ce38 100644 --- a/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts +++ b/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts @@ -16,8 +16,16 @@ export class MessageFlagsNotNull1713116476900 implements MigrationInterface { await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_old;"); } - public async down(): Promise { - // dont care - throw new Error("Migration down is not implemented."); + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE messages RENAME COLUMN flags TO flags_new;", + ); + await queryRunner.query( + "ALTER TABLE messages ADD COLUMN flags integer;", + ); + await queryRunner.query( + "UPDATE messages SET flags = flags_new;", + ); + await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_new;"); } } diff --git a/src/util/migration/postgres/1720628601997-badges.ts b/src/util/migration/postgres/1720628601997-badges.ts index f7b9958bf..0cecb6ea1 100644 --- a/src/util/migration/postgres/1720628601997-badges.ts +++ b/src/util/migration/postgres/1720628601997-badges.ts @@ -12,5 +12,6 @@ export class Badges1720628601997 implements MigrationInterface { public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "badge_ids"`); + await queryRunner.query(`DROP TABLE "badges"`); } } diff --git a/src/util/migration/postgres/1752321571508-RoleColors.ts b/src/util/migration/postgres/1752321571508-RoleColors.ts index 687142e3c..beb42062f 100644 --- a/src/util/migration/postgres/1752321571508-RoleColors.ts +++ b/src/util/migration/postgres/1752321571508-RoleColors.ts @@ -5,6 +5,8 @@ export class RoleColors1752321571508 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "roles" ADD "colors" text`); + await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`); + await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts b/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts deleted file mode 100644 index 36c24a375..000000000 --- a/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class RoleColorsSolidColor1752342900886 implements MigrationInterface { - name = 'RoleColorsSolidColor1752342900886' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`); - await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" DROP NOT NULL`); - } - -} diff --git a/src/util/migration/postgres/1752383879533-message_pinned_at.ts b/src/util/migration/postgres/1752383879533-message_pinned_at.ts index 2e294aadf..9f71f5fa6 100644 --- a/src/util/migration/postgres/1752383879533-message_pinned_at.ts +++ b/src/util/migration/postgres/1752383879533-message_pinned_at.ts @@ -14,12 +14,12 @@ export class MessagePinnedAt1752383879533 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "messages" DROP COLUMN "pinned_at"`, - ); await queryRunner.query(`ALTER TABLE "messages" ADD "pinned" boolean`); await queryRunner.query( `UPDATE "messages" SET "pinned" = true WHERE "pinned_at" IS NOT NULL`, ); + await queryRunner.query( + `ALTER TABLE "messages" DROP COLUMN "pinned_at"`, + ); } } diff --git a/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts b/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts index 02f8a9340..e32099276 100644 --- a/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts +++ b/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class DeleteBotUsersWithoutAnApplication1761113394664 implements MigrationInterface { + name = "DeleteBotUsersWithoutAnApplication1761113394664"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`DELETE FROM users WHERE bot = true AND id NOT IN (SELECT bot_user_id FROM applications);`); } diff --git a/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts b/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts index 47de75068..0e17bf814 100644 --- a/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts +++ b/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class FixGifStickersFormatType1762611552514 implements MigrationInterface { + name = "FixGifStickersFormatType1762611552514"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`UPDATE "stickers" SET "format_type" = 4 WHERE "format_type" = 0;`); } diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts index 50763dd9b..e5839047e 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts @@ -33,12 +33,9 @@ if (!process.env) { config({ quiet: true }); } -const dbConnectionString = - process.env.DATABASE || path.join(process.cwd(), "database.db"); +const dbConnectionString = process.env.DATABASE || path.join(process.cwd(), "database.db"); -export const DatabaseType = dbConnectionString.includes("://") - ? dbConnectionString.split(":")[0]?.replace("+srv", "") - : "sqlite"; +export const DatabaseType = dbConnectionString.includes("://") ? dbConnectionString.split(":")[0]?.replace("+srv", "") : "sqlite"; const isSqlite = DatabaseType.includes("sqlite"); export const DataSourceOptions = new DataSource({ @@ -69,11 +66,7 @@ export async function initDatabase(): Promise { if (dbConnection) return dbConnection; if (isSqlite) { - console.log( - `[Database] ${red( - `You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`, - )}`, - ); + console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`); } if (!process.env.DB_SYNC) { @@ -104,26 +97,14 @@ export async function initDatabase(): Promise { } }; if (!(await dbExists())) { - console.log( - "[Database] This appears to be a fresh database. Synchronising.", - ); - await dbConnection.synchronize(); - - // On next start, typeorm will try to run all the migrations again from beginning. - // Manually insert every current migration to prevent this: - await Promise.all( - dbConnection.migrations.map((migration) => - Migration.insert({ - name: migration.name, - timestamp: Date.now(), - }), - ), - ); - } else { - console.log("[Database] Applying missing migrations, if any."); - await dbConnection.runMigrations(); + console.log("[Database] This appears to be a fresh database. Running initial DDL."); + const qr = dbConnection.createQueryRunner(); + await new (require("../migration/postgres-initial").initial0)().up(qr); } + console.log("[Database] Applying missing migrations, if any."); + await dbConnection.runMigrations(); + console.log(`[Database] ${green("Connected")}`); return dbConnection; From f60ceae1a009354e52528139293b09ea7bd96e7f Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 23:12:23 +0100 Subject: [PATCH 23/34] Fix sqlite --- src/util/util/Database.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts index e5839047e..998b8e4c6 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts @@ -22,6 +22,7 @@ import { green, red, yellow } from "picocolors"; import { DataSource } from "typeorm"; import { ConfigEntity } from "../entities/Config"; import { Migration } from "../entities/Migration"; +import fs from "fs"; // UUID extension option is only supported with postgres // We want to generate all id's with Snowflakes that's why we have our own BaseEntity class @@ -96,10 +97,12 @@ export async function initDatabase(): Promise { return false; } }; + if (!(await dbExists())) { console.log("[Database] This appears to be a fresh database. Running initial DDL."); const qr = dbConnection.createQueryRunner(); - await new (require("../migration/postgres-initial").initial0)().up(qr); + if (fs.existsSync(path.join(__dirname, "..", "migration", DatabaseType, "initial0.js"))) + await new (require(`../migration/${DatabaseType}-initial`).initial0)().up(qr); } console.log("[Database] Applying missing migrations, if any."); From 4443b76dc75c208edb90c6e9540ac8f3b1a1b299 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 23:15:53 +0100 Subject: [PATCH 24/34] Regenerate spacebar db for admin api --- .../Spacebar.Db/Contexts/SpacebarDbContext.cs | 41 ++++++++---- .../Spacebar.Db/Models/Application.cs | 12 ++-- .../Spacebar.Db/Models/ApplicationCommand.cs | 66 +++++++++++++++++++ extra/admin-api/Spacebar.Db/Models/Ban.cs | 2 +- .../Spacebar.Db/Models/InstanceBan.cs | 47 +++++++++++++ extra/admin-api/Spacebar.Db/Models/Message.cs | 19 +++--- extra/admin-api/Spacebar.Db/Models/Session.cs | 8 +-- .../Spacebar.Db/Models/TeamMember.cs | 6 +- extra/admin-api/Spacebar.Db/Models/User.cs | 22 +++---- .../Spacebar.Db/Models/UserSetting.cs | 6 +- .../admin-api/Spacebar.Db/Spacebar.Db.csproj | 7 +- 11 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 extra/admin-api/Spacebar.Db/Models/ApplicationCommand.cs create mode 100644 extra/admin-api/Spacebar.Db/Models/InstanceBan.cs diff --git a/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs b/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs index 516995130..0a707c757 100644 --- a/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs +++ b/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Spacebar.Db.Models; -using Stream = Spacebar.Db.Models.Stream; namespace Spacebar.Db.Contexts; @@ -15,6 +14,8 @@ public partial class SpacebarDbContext : DbContext public virtual DbSet Applications { get; set; } + public virtual DbSet ApplicationCommands { get; set; } + public virtual DbSet Attachments { get; set; } public virtual DbSet AuditLogs { get; set; } @@ -47,6 +48,8 @@ public partial class SpacebarDbContext : DbContext public virtual DbSet Guilds { get; set; } + public virtual DbSet InstanceBans { get; set; } + public virtual DbSet Invites { get; set; } public virtual DbSet Members { get; set; } @@ -120,6 +123,16 @@ public partial class SpacebarDbContext : DbContext .HasConstraintName("FK_a36ed02953077f408d0f3ebc424"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK_0f73c2f025989c407947e1f75fe"); + + entity.Property(e => e.DmPermission).HasDefaultValue(true); + entity.Property(e => e.Options).HasDefaultValueSql("'[]'::text"); + entity.Property(e => e.Type).HasDefaultValue(1); + entity.Property(e => e.Version).HasDefaultValueSql("'0'::character varying"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("PK_5e1f050bcff31e3084a1d662412"); @@ -266,6 +279,17 @@ public partial class SpacebarDbContext : DbContext entity.HasOne(d => d.WidgetChannel).WithMany(p => p.GuildWidgetChannels).HasConstraintName("FK_9d1d665379eefde7876a17afa99"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK_3aa6e80a6d325601054892b1340"); + + entity.Property(e => e.CreatedAt).HasDefaultValueSql("now()"); + + entity.HasOne(d => d.OriginInstanceBan).WithOne(p => p.InverseOriginInstanceBan) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_0b02d18d0d830f160c921192a30"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Code).HasName("PK_33fd8a248db1cd832baa8aa25bf"); @@ -321,7 +345,6 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_18325f38ae6de43878487eff986"); - entity.Property(e => e.Flags).HasDefaultValue(0); entity.Property(e => e.Timestamp).HasDefaultValueSql("now()"); entity.HasOne(d => d.Application).WithMany(p => p.Messages).HasConstraintName("FK_5d3ec1cb962de6488637fd779d6"); @@ -342,7 +365,9 @@ public partial class SpacebarDbContext : DbContext .OnDelete(DeleteBehavior.Cascade) .HasConstraintName("FK_b0525304f2262b7014245351c76"); - entity.HasOne(d => d.MessageReferenceNavigation).WithMany(p => p.InverseMessageReferenceNavigation).HasConstraintName("FK_61a92bb65b302a76d9c1fcd3174"); + entity.HasOne(d => d.MessageReferenceNavigation).WithMany(p => p.InverseMessageReferenceNavigation) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_61a92bb65b302a76d9c1fcd3174"); entity.HasOne(d => d.Webhook).WithMany(p => p.Messages).HasConstraintName("FK_f83c04bcf1df4e5c0e7a52ed348"); @@ -475,8 +500,6 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_de8fc5a9c364568f294798fe1e9"); - entity.Property(e => e.Closed).HasDefaultValue(false); - entity.HasOne(d => d.Channel).WithMany(p => p.Recipients).HasConstraintName("FK_2f18ee1ba667f233ae86c0ea60e"); entity.HasOne(d => d.User).WithMany(p => p.Recipients).HasConstraintName("FK_6157e8b6ba4e6e3089616481fe2"); @@ -495,8 +518,6 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_c1433d71a4838793a49dcad46ab"); - entity.Property(e => e.Flags).HasDefaultValue(0); - entity.HasOne(d => d.Guild).WithMany(p => p.Roles).HasConstraintName("FK_c32c1ab1c4dc7dcb0278c4b1b8b"); }); @@ -518,6 +539,8 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_3238ef96f18b355b671619111bc"); + entity.Property(e => e.Activities).HasDefaultValueSql("'[]'::text"); + entity.HasOne(d => d.User).WithMany(p => p.Sessions) .OnDelete(DeleteBehavior.Cascade) .HasConstraintName("FK_085d540d9f418cfbdc7bd55bb19"); @@ -560,8 +583,6 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_49bdc3f66394c12478f8371c546"); - entity.Property(e => e.Used).HasDefaultValue(false); - entity.HasOne(d => d.Stream).WithMany(p => p.StreamSessions).HasConstraintName("FK_8b5a028a34dae9ee54af37c9c32"); entity.HasOne(d => d.User).WithMany(p => p.StreamSessions).HasConstraintName("FK_13ae5c29aff4d0890c54179511a"); @@ -602,8 +623,6 @@ public partial class SpacebarDbContext : DbContext { entity.HasKey(e => e.Id).HasName("PK_a3ffb1c0c8416b9fc6f907b7433"); - entity.Property(e => e.WebauthnEnabled).HasDefaultValue(false); - entity.HasOne(d => d.SettingsIndexNavigation).WithOne(p => p.User).HasConstraintName("FK_0c14beb78d8c5ccba66072adbc7"); }); diff --git a/extra/admin-api/Spacebar.Db/Models/Application.cs b/extra/admin-api/Spacebar.Db/Models/Application.cs index 27d8a45b5..a5dda6f85 100644 --- a/extra/admin-api/Spacebar.Db/Models/Application.cs +++ b/extra/admin-api/Spacebar.Db/Models/Application.cs @@ -86,12 +86,6 @@ public partial class Application [Column("privacy_policy_url", TypeName = "character varying")] public string? PrivacyPolicyUrl { get; set; } - [Column("guild_id", TypeName = "character varying")] - public string? GuildId { get; set; } - - [Column("custom_install_url", TypeName = "character varying")] - public string? CustomInstallUrl { get; set; } - [Column("owner_id", TypeName = "character varying")] public string? OwnerId { get; set; } @@ -101,6 +95,12 @@ public partial class Application [Column("team_id", TypeName = "character varying")] public string? TeamId { get; set; } + [Column("guild_id", TypeName = "character varying")] + public string? GuildId { get; set; } + + [Column("custom_install_url", TypeName = "character varying")] + public string? CustomInstallUrl { get; set; } + [ForeignKey("BotUserId")] [InverseProperty("ApplicationBotUser")] public virtual User? BotUser { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Models/ApplicationCommand.cs b/extra/admin-api/Spacebar.Db/Models/ApplicationCommand.cs new file mode 100644 index 000000000..96c6d2aec --- /dev/null +++ b/extra/admin-api/Spacebar.Db/Models/ApplicationCommand.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Spacebar.Db.Models; + +[Table("application_commands")] +public partial class ApplicationCommand +{ + [Key] + [Column("id", TypeName = "character varying")] + public string Id { get; set; } = null!; + + [Column("type")] + public int Type { get; set; } + + [Column("application_id", TypeName = "character varying")] + public string ApplicationId { get; set; } = null!; + + [Column("guild_id", TypeName = "character varying")] + public string? GuildId { get; set; } + + [Column("name", TypeName = "character varying")] + public string Name { get; set; } = null!; + + [Column("name_localizations")] + public string? NameLocalizations { get; set; } + + [Column("description", TypeName = "character varying")] + public string Description { get; set; } = null!; + + [Column("description_localizations")] + public string? DescriptionLocalizations { get; set; } + + [Column("options")] + public string Options { get; set; } = null!; + + [Column("default_member_permissions", TypeName = "character varying")] + public string? DefaultMemberPermissions { get; set; } + + [Column("dm_permission")] + public bool DmPermission { get; set; } + + [Column("permissions")] + public string? Permissions { get; set; } + + [Column("nsfw")] + public bool Nsfw { get; set; } + + [Column("integration_types")] + public string? IntegrationTypes { get; set; } + + [Column("global_popularity_rank")] + public int GlobalPopularityRank { get; set; } + + [Column("contexts")] + public string? Contexts { get; set; } + + [Column("version", TypeName = "character varying")] + public string Version { get; set; } = null!; + + [Column("handler")] + public int Handler { get; set; } +} diff --git a/extra/admin-api/Spacebar.Db/Models/Ban.cs b/extra/admin-api/Spacebar.Db/Models/Ban.cs index 1b6665d95..bea498426 100644 --- a/extra/admin-api/Spacebar.Db/Models/Ban.cs +++ b/extra/admin-api/Spacebar.Db/Models/Ban.cs @@ -23,7 +23,7 @@ public partial class Ban public string? ExecutorId { get; set; } [Column("ip", TypeName = "character varying")] - public string Ip { get; set; } = null!; + public string? Ip { get; set; } [Column("reason", TypeName = "character varying")] public string? Reason { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Models/InstanceBan.cs b/extra/admin-api/Spacebar.Db/Models/InstanceBan.cs new file mode 100644 index 000000000..92d510fb4 --- /dev/null +++ b/extra/admin-api/Spacebar.Db/Models/InstanceBan.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Spacebar.Db.Models; + +[Table("instance_bans")] +[Index("OriginInstanceBanId", Name = "REL_0b02d18d0d830f160c921192a3", IsUnique = true)] +public partial class InstanceBan +{ + [Key] + [Column("id", TypeName = "character varying")] + public string Id { get; set; } = null!; + + [Column("created_at", TypeName = "timestamp without time zone")] + public DateTime CreatedAt { get; set; } + + [Column("reason", TypeName = "character varying")] + public string Reason { get; set; } = null!; + + [Column("user_id", TypeName = "character varying")] + public string? UserId { get; set; } + + [Column("fingerprint", TypeName = "character varying")] + public string? Fingerprint { get; set; } + + [Column("ip_address", TypeName = "character varying")] + public string? IpAddress { get; set; } + + [Column("is_from_other_instance_ban")] + public bool IsFromOtherInstanceBan { get; set; } + + [Column("origin_instance_ban_id", TypeName = "character varying")] + public string? OriginInstanceBanId { get; set; } + + [Column("is_allowlisted")] + public bool IsAllowlisted { get; set; } + + [InverseProperty("OriginInstanceBan")] + public virtual InstanceBan? InverseOriginInstanceBan { get; set; } + + [ForeignKey("OriginInstanceBanId")] + [InverseProperty("InverseOriginInstanceBan")] + public virtual InstanceBan? OriginInstanceBan { get; set; } +} diff --git a/extra/admin-api/Spacebar.Db/Models/Message.cs b/extra/admin-api/Spacebar.Db/Models/Message.cs index 35de4819e..11cee7380 100644 --- a/extra/admin-api/Spacebar.Db/Models/Message.cs +++ b/extra/admin-api/Spacebar.Db/Models/Message.cs @@ -58,18 +58,12 @@ public partial class Message [Column("nonce")] public string? Nonce { get; set; } - [Column("pinned_at", TypeName = "timestamp without time zone")] - public DateTime? PinnedAt { get; set; } - [Column("type")] public int Type { get; set; } [Column("activity")] public string? Activity { get; set; } - [Column("flags")] - public int Flags { get; set; } - [Column("message_reference")] public string? MessageReference { get; set; } @@ -79,6 +73,12 @@ public partial class Message [Column("components")] public string? Components { get; set; } + [Column("message_reference_id", TypeName = "character varying")] + public string? MessageReferenceId { get; set; } + + [Column("flags")] + public int Flags { get; set; } + [Column("poll")] public string? Poll { get; set; } @@ -88,8 +88,11 @@ public partial class Message [Column("avatar", TypeName = "character varying")] public string? Avatar { get; set; } - [Column("message_reference_id", TypeName = "character varying")] - public string? MessageReferenceId { get; set; } + [Column("pinned_at", TypeName = "timestamp without time zone")] + public DateTime? PinnedAt { get; set; } + + [Column("interaction_metadata")] + public string? InteractionMetadata { get; set; } [ForeignKey("ApplicationId")] [InverseProperty("Messages")] diff --git a/extra/admin-api/Spacebar.Db/Models/Session.cs b/extra/admin-api/Spacebar.Db/Models/Session.cs index b27ad1f32..f7eda3cfe 100644 --- a/extra/admin-api/Spacebar.Db/Models/Session.cs +++ b/extra/admin-api/Spacebar.Db/Models/Session.cs @@ -20,17 +20,17 @@ public partial class Session public string SessionId { get; set; } = null!; [Column("activities")] - public string? Activities { get; set; } + public string Activities { get; set; } = null!; [Column("client_info")] public string ClientInfo { get; set; } = null!; - [Column("client_status")] - public string ClientStatus { get; set; } = null!; - [Column("status", TypeName = "character varying")] public string Status { get; set; } = null!; + [Column("client_status")] + public string ClientStatus { get; set; } = null!; + [ForeignKey("UserId")] [InverseProperty("Sessions")] public virtual User? User { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Models/TeamMember.cs b/extra/admin-api/Spacebar.Db/Models/TeamMember.cs index ec1cf646f..0d5c453c4 100644 --- a/extra/admin-api/Spacebar.Db/Models/TeamMember.cs +++ b/extra/admin-api/Spacebar.Db/Models/TeamMember.cs @@ -19,15 +19,15 @@ public partial class TeamMember [Column("permissions")] public string Permissions { get; set; } = null!; - [Column("role", TypeName = "character varying")] - public string Role { get; set; } = null!; - [Column("team_id", TypeName = "character varying")] public string? TeamId { get; set; } [Column("user_id", TypeName = "character varying")] public string? UserId { get; set; } + [Column("role", TypeName = "character varying")] + public string Role { get; set; } = null!; + [ForeignKey("TeamId")] [InverseProperty("TeamMembers")] public virtual Team? Team { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Models/User.cs b/extra/admin-api/Spacebar.Db/Models/User.cs index b16d3bae8..7af60bf48 100644 --- a/extra/admin-api/Spacebar.Db/Models/User.cs +++ b/extra/admin-api/Spacebar.Db/Models/User.cs @@ -65,9 +65,6 @@ public partial class User [Column("mfa_enabled")] public bool MfaEnabled { get; set; } - [Column("webauthn_enabled")] - public bool WebauthnEnabled { get; set; } - [Column("totp_secret", TypeName = "character varying")] public string? TotpSecret { get; set; } @@ -92,20 +89,20 @@ 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 long Flags { get; set; } [Column("public_flags")] - public ulong PublicFlags { get; set; } + public long PublicFlags { get; set; } [Column("purchased_flags")] - public int PurchasedFlags { get; set; } + public long PurchasedFlags { get; set; } [Column("premium_usage_flags")] public int PremiumUsageFlags { get; set; } [Column("rights")] - public ulong Rights { get; set; } + public long Rights { get; set; } [Column("data")] public string Data { get; set; } = null!; @@ -116,12 +113,15 @@ public partial class User [Column("extended_settings")] public string ExtendedSettings { get; set; } = null!; - [Column("badge_ids")] - public string? BadgeIds { get; set; } - [Column("settingsIndex")] public int? SettingsIndex { get; set; } + [Column("webauthn_enabled")] + public bool WebauthnEnabled { get; set; } + + [Column("badge_ids")] + public string? BadgeIds { get; set; } + [InverseProperty("BotUser")] public virtual Application? ApplicationBotUser { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Models/UserSetting.cs b/extra/admin-api/Spacebar.Db/Models/UserSetting.cs index c742b0925..5bd3354f5 100644 --- a/extra/admin-api/Spacebar.Db/Models/UserSetting.cs +++ b/extra/admin-api/Spacebar.Db/Models/UserSetting.cs @@ -52,9 +52,6 @@ public partial class UserSetting [Column("explicit_content_filter")] public int? ExplicitContentFilter { get; set; } - [Column("friend_discovery_flags")] - public int? FriendDiscoveryFlags { get; set; } - [Column("friend_source_flags")] public string? FriendSourceFlags { get; set; } @@ -109,6 +106,9 @@ public partial class UserSetting [Column("timezone_offset")] public int? TimezoneOffset { get; set; } + [Column("friend_discovery_flags")] + public int? FriendDiscoveryFlags { get; set; } + [Column("view_nsfw_guilds")] public bool? ViewNsfwGuilds { get; set; } diff --git a/extra/admin-api/Spacebar.Db/Spacebar.Db.csproj b/extra/admin-api/Spacebar.Db/Spacebar.Db.csproj index 350753bfb..2d1dcaf5b 100644 --- a/extra/admin-api/Spacebar.Db/Spacebar.Db.csproj +++ b/extra/admin-api/Spacebar.Db/Spacebar.Db.csproj @@ -7,11 +7,8 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 0be92b274a589beb8ea047d527b67ae7f3678092 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 23:34:36 +0100 Subject: [PATCH 25/34] Update admin api db model to match TS --- .../Controllers/UserController.cs | 2 +- .../Spacebar.Db/Contexts/SpacebarDbContext.cs | 5 ++-- extra/admin-api/Spacebar.Db/Models/User.cs | 8 +++--- .../Spacebar.AdminAPITest.csproj | 4 +-- .../db-patches/00-use-stream-class.patch | 14 ++++++++++ .../db-patches/01-ulong-user-rights.patch | 28 +++++++++++++++++++ .../db-patches/db-00-fix-flags.patch | 13 --------- extra/admin-api/scaffold-db | 10 +++++-- 8 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 extra/admin-api/db-patches/00-use-stream-class.patch create mode 100644 extra/admin-api/db-patches/01-ulong-user-rights.patch delete mode 100644 extra/admin-api/db-patches/db-00-fix-flags.patch diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs index 436c0f91f..c727077ab 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs @@ -48,7 +48,7 @@ public class UserController(ILogger logger, Configuration config Disabled = x.Disabled, Deleted = x.Deleted, Email = x.Email, - Flags = ulong.Parse(x.Flags), + Flags = x.Flags, PublicFlags = x.PublicFlags, Rights = x.Rights, ApplicationBotUser = x.ApplicationBotUser == null ? null : new(), diff --git a/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs b/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs index 0a707c757..7d380b1c3 100644 --- a/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs +++ b/extra/admin-api/Spacebar.Db/Contexts/SpacebarDbContext.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Spacebar.Db.Models; +using Stream = Spacebar.Db.Models.Stream; namespace Spacebar.Db.Contexts; diff --git a/extra/admin-api/Spacebar.Db/Models/User.cs b/extra/admin-api/Spacebar.Db/Models/User.cs index 7af60bf48..323846d46 100644 --- a/extra/admin-api/Spacebar.Db/Models/User.cs +++ b/extra/admin-api/Spacebar.Db/Models/User.cs @@ -90,19 +90,19 @@ public partial class User public string? Email { get; set; } [Column("flags")] - public long Flags { get; set; } + public ulong Flags { get; set; } [Column("public_flags")] - public long PublicFlags { get; set; } + public ulong PublicFlags { get; set; } [Column("purchased_flags")] - public long PurchasedFlags { get; set; } + public ulong PurchasedFlags { get; set; } [Column("premium_usage_flags")] 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!; diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj index ec27f0b60..5daabdd0a 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj +++ b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj @@ -10,9 +10,7 @@ - - ..\..\..\..\..\..\..\.nuget\packages\arcanelibs\1.0.0-preview.20241210-161342\lib\net10.0\ArcaneLibs.dll - + diff --git a/extra/admin-api/db-patches/00-use-stream-class.patch b/extra/admin-api/db-patches/00-use-stream-class.patch new file mode 100644 index 000000000..1636f1202 --- /dev/null +++ b/extra/admin-api/db-patches/00-use-stream-class.patch @@ -0,0 +1,14 @@ +diff --git a/Spacebar.Db/Contexts/SpacebarDbContext.cs b/Spacebar.Db/Contexts/SpacebarDbContext.cs +index 0a707c75..7d380b1c 100644 +--- a/Spacebar.Db/Contexts/SpacebarDbContext.cs ++++ b/Spacebar.Db/Contexts/SpacebarDbContext.cs +@@ -1,7 +1,6 @@ +-using System; +-using System.Collections.Generic; +-using Microsoft.EntityFrameworkCore; ++using Microsoft.EntityFrameworkCore; + using Spacebar.Db.Models; ++using Stream = Spacebar.Db.Models.Stream; + + namespace Spacebar.Db.Contexts; + diff --git a/extra/admin-api/db-patches/01-ulong-user-rights.patch b/extra/admin-api/db-patches/01-ulong-user-rights.patch new file mode 100644 index 000000000..362b67d7d --- /dev/null +++ b/extra/admin-api/db-patches/01-ulong-user-rights.patch @@ -0,0 +1,28 @@ +diff --git a/Spacebar.Db/Models/User.cs b/Spacebar.Db/Models/User.cs +index 7af60bf4..323846d4 100644 +--- a/Spacebar.Db/Models/User.cs ++++ b/Spacebar.Db/Models/User.cs +@@ -90,19 +90,19 @@ public partial class User + public string? Email { get; set; } + + [Column("flags")] +- public long Flags { get; set; } ++ public ulong Flags { get; set; } + + [Column("public_flags")] +- public long PublicFlags { get; set; } ++ public ulong PublicFlags { get; set; } + + [Column("purchased_flags")] +- public long PurchasedFlags { get; set; } ++ public ulong PurchasedFlags { get; set; } + + [Column("premium_usage_flags")] + 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!; diff --git a/extra/admin-api/db-patches/db-00-fix-flags.patch b/extra/admin-api/db-patches/db-00-fix-flags.patch deleted file mode 100644 index 437368f49..000000000 --- a/extra/admin-api/db-patches/db-00-fix-flags.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- Spacebar.Db/Models/User.cs.orig 2025-10-05 22:04:37.168566856 +0200 -+++ Spacebar.Db/Models/User.cs 2025-10-05 22:07:11.519980808 +0200 -@@ -92,8 +92,8 @@ - [Column("email", TypeName = "character varying")] - public string? Email { get; set; } - -- [Column("flags")] -- public int Flags { get; set; } -+ [Column("flags", TypeName = "character varying")] -+ public string Flags { get; set; } - - [Column("public_flags")] - public int PublicFlags { get; set; } diff --git a/extra/admin-api/scaffold-db b/extra/admin-api/scaffold-db index 0f332e005..19d321306 100755 --- a/extra/admin-api/scaffold-db +++ b/extra/admin-api/scaffold-db @@ -19,6 +19,8 @@ rm Class1.cs dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL -n -f net9.0 dotnet add package Microsoft.EntityFrameworkCore.Design -n -f net9.0 +dotnet restore + dotnet-ef dbcontext scaffold "Host=127.0.0.1; Username=postgres; Database=sb-server-scaffold" \ Npgsql.EntityFrameworkCore.PostgreSQL \ -o Models \ @@ -28,6 +30,8 @@ dotnet-ef dbcontext scaffold "Host=127.0.0.1; Username=postgres; Database=sb-ser --no-onconfiguring \ --data-annotations -for patch in db-patches/*.patch; do - patch -p3 < $patch -done \ No newline at end of file +for patch in ../db-patches/*.patch; do + patch -p1 < $patch +done + +echo 'Scaffolded database and applied patches. Dont forget to generate new patches with `git diff --relative Spacebar.Db/path/xyz > db-patches/001-your-patch-name.patch!`' \ No newline at end of file From 4bb3ff1aaee9a1c81bd36646ed1c3b6d214d94bc Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 23:41:48 +0100 Subject: [PATCH 26/34] Drop nonfunctional default ipdata key --- src/util/config/types/SecurityConfiguration.ts | 2 +- .../postgres/1765665440000-DropDefaultIPDataKey.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 9fbf5d218..078b90459 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -36,7 +36,7 @@ export class SecurityConfiguration { // https://docs.abuseipdb.com/#api-daily-rate-limits abuseipdbBlacklistRatelimit: number = 5; abuseipdbConfidenceScoreTreshold: number = 50; - ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; // isnt even valid anymore it seems? + ipdataApiKey: string | null = null; mfaBackupCodeCount: number = 10; statsWorldReadable: boolean = true; defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week diff --git a/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts b/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts new file mode 100644 index 000000000..ad1b736f7 --- /dev/null +++ b/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DropDefaultIPDataKey1765665440000 implements MigrationInterface { + name = 'DropDefaultIPDataKey1765665440000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE "config" SET "value" = NULL WHERE "key" = 'security_ipdataApiKey' AND "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE "config" SET "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"' WHERE "key" = 'security_ipdataApiKey' AND "value" IS NULL`); + } + +} From 03e6f8c7f2e751e298755f9d11c95c441393826f Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 13 Dec 2025 23:45:33 +0100 Subject: [PATCH 27/34] Fix missing config usage for backup codes --- src/util/entities/BackupCodes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts index 31c763434..a0c9ba4bd 100644 --- a/src/util/entities/BackupCodes.ts +++ b/src/util/entities/BackupCodes.ts @@ -20,6 +20,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; import crypto from "crypto"; +import { Config } from "../util"; @Entity({ name: "backup_codes", @@ -41,7 +42,7 @@ export class BackupCode extends BaseClass { export function generateMfaBackupCodes(user_id: string) { const backup_codes: BackupCode[] = []; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) { const code = BackupCode.create({ user: { id: user_id }, code: crypto.randomBytes(4).toString("hex"), // 8 characters From f3f8c1c9f8c30a491a3aed61e617b54cbc2f0070 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 14 Dec 2025 00:13:52 +0100 Subject: [PATCH 28/34] force sync for sqlite --- src/util/util/Database.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts index 998b8e4c6..78712b9e7 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts @@ -88,6 +88,13 @@ export async function initDatabase(): Promise { dbConnection = await DataSourceOptions.initialize(); + if (DatabaseType === "sqlite") { + console.log(`[Database] ${yellow("Warning: SQLite is not supported. Forcing sync, this may lead to data loss!")}`); + await dbConnection.synchronize(); + console.log(`[Database] ${green("Connected")}`); + return dbConnection; + } + // Crude way of detecting if the migrations table exists. const dbExists = async () => { try { From a4527d166a4a9c56089a6034eac8bbf311c3b952 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 14 Dec 2025 00:49:13 +0100 Subject: [PATCH 29/34] No longer generate security_jwtSecret, switch webauthn to modern tokens, dont --- .../config/types/SecurityConfiguration.ts | 2 +- src/util/util/Token.ts | 4 +- src/util/util/WebAuthn.ts | 41 +++++++++++-------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 078b90459..1f3a19181 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -24,7 +24,7 @@ export class SecurityConfiguration { twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration(); autoUpdate: boolean | number = true; requestSignature: string = crypto.randomBytes(32).toString("base64"); - jwtSecret: string = crypto.randomBytes(256).toString("base64"); // deprecated + jwtSecret: string | null = null; // header to get the real user ip address // X-Forwarded-For for nginx/reverse proxies // CF-Connecting-IP for cloudflare diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index db2a313b9..9e071ed98 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -104,8 +104,8 @@ export const checkToken = ( if (!dec) return reject("Could not parse token"); logAuth("Decoded token: " + JSON.stringify(dec)); - if (dec.header.alg == "HS256") { - jwt.verify(token, Config.get().security.jwtSecret, JWTOptions, validateUser); + if (dec.header.alg == "HS256" && Config.get().security.jwtSecret !== null) { + jwt.verify(token, Config.get().security.jwtSecret!, JWTOptions, validateUser); } else if (dec.header.alg == "ES512") { loadOrGenerateKeypair().then((keyPair) => { jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser); diff --git a/src/util/util/WebAuthn.ts b/src/util/util/WebAuthn.ts index a746f3646..d499097d9 100644 --- a/src/util/util/WebAuthn.ts +++ b/src/util/util/WebAuthn.ts @@ -19,13 +19,14 @@ import { Fido2Lib } from "fido2-lib"; import jwt from "jsonwebtoken"; import { Config } from "./Config"; +import { loadOrGenerateKeypair } from "./Token"; const jwtSignOptions: jwt.SignOptions = { - algorithm: "HS256", + algorithm: "ES512", expiresIn: "5m", }; const jwtVerifyOptions: jwt.VerifyOptions = { - algorithms: ["HS256"], + algorithms: ["ES512"], }; export const WebAuthn: { @@ -44,28 +45,32 @@ export async function generateWebAuthnTicket( challenge: string, ): Promise { return new Promise((res, rej) => { - jwt.sign( - { challenge }, - Config.get().security.jwtSecret, - jwtSignOptions, - (err, token) => { - if (err || !token) return rej(err || "no token"); - return res(token); - }, + loadOrGenerateKeypair().then(kp=> + jwt.sign( + { challenge }, + kp.privateKey, + jwtSignOptions, + (err, token) => { + if (err || !token) return rej(err || "no token"); + return res(token); + }, + ) ); }); } export async function verifyWebAuthnToken(token: string) { return new Promise((res, rej) => { - jwt.verify( - token, - Config.get().security.jwtSecret, - jwtVerifyOptions, - async (err, decoded) => { - if (err) return rej(err); - return res(decoded); - }, + loadOrGenerateKeypair().then(kp=> + jwt.verify( + token, + kp.publicKey, + jwtVerifyOptions, + async (err, decoded) => { + if (err) return rej(err); + return res(decoded); + }, + ) ); }); } From 4534d4dbf5f5071fa71c5dec4444bf4b2bbd040c Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 14 Dec 2025 23:18:35 +0100 Subject: [PATCH 30/34] admin api packaging --- .gitignore | 4 +- extra/admin-api/.gitignore | 2 +- extra/admin-api/ConfigTest/ConfigTest.csproj | 20 + extra/admin-api/ConfigTest/Program.cs | 14 + .../ConfigTest/Properties/launchSettings.json | 12 + extra/admin-api/ConfigTest/Worker.cs | 43 ++ .../appsettings.Development.json | 0 .../appsettings.json | 0 .../Controllers/ConfigController.cs | 61 +++ .../Controllers/GuildController.cs | 6 +- .../Controllers/Media/UserMediaController.cs | 6 +- .../Controllers/PingController.cs | 4 +- .../Controllers/UserController.cs | 6 +- .../Extensions/DbExtensions.cs | 2 +- .../Middleware/AuthenticationMiddleware.cs | 4 +- .../Program.cs | 4 +- .../Properties/launchSettings.json | 0 .../Services/AuthenticationService.cs | 2 +- .../Services/Configuration.cs | 2 +- .../Spacebar.AdminApi.csproj} | 1 + .../appsettings.Development.json | 27 + .../Spacebar.AdminApi/appsettings.json | 9 + extra/admin-api/Spacebar.AdminApi/deps.json | 447 ++++++++++++++++ .../Spacebar.CleanSettingsRows/deps.json | 492 ++++++++++++++++++ .../admin-api/Spacebar.ConfigModel/Class1.cs | 64 +++ .../Extensions/JsonExtensions.cs | 119 +++++ .../Spacebar.ConfigModel.csproj | 9 + extra/admin-api/Spacebar.Db/deps.json | 362 +++++++++++++ extra/admin-api/SpacebarAdminAPI.sln | 18 +- .../App.razor | 0 .../Classes/OpenAPI/OpenAPISchema.cs | 2 +- .../Layout/MainLayout.razor | 0 .../Layout/MainLayout.razor.css | 0 .../Layout/NavMenu.razor | 2 +- .../Layout/NavMenu.razor.css | 0 .../Pages/Guilds.razor | 2 +- .../Pages/Home.razor | 2 +- .../Pages/HttpTestClient.razor | 7 +- .../OpenAPIParameterDescription.razor | 2 +- .../Pages/Login.razor | 2 +- .../Pages/Media/Index.razor | 0 .../Pages/Media/Users.razor | 2 +- .../Pages/Users.razor | 2 +- .../Pages/UsersDelete.razor | 2 +- .../Program.cs | 4 +- .../Properties/launchSettings.json | 0 .../Services/Config.cs | 2 +- .../Services/StreamingHttpClient.cs | 21 +- .../Spacebar.AdminApi.TestClient.csproj} | 4 +- .../_Imports.razor | 4 +- .../Spacebar.AdminApi.TestClient/deps.json | 217 ++++++++ .../wwwroot/appsettings.json | 0 .../wwwroot/css/app.css | 0 .../wwwroot/favicon.png | Bin .../wwwroot/icon-192.png | Bin .../wwwroot/index.html | 2 +- .../lib/bootstrap/dist/css/bootstrap-grid.css | 0 .../bootstrap/dist/css/bootstrap-grid.css.map | 0 .../bootstrap/dist/css/bootstrap-grid.min.css | 0 .../dist/css/bootstrap-grid.min.css.map | 0 .../bootstrap/dist/css/bootstrap-grid.rtl.css | 0 .../dist/css/bootstrap-grid.rtl.css.map | 0 .../dist/css/bootstrap-grid.rtl.min.css | 0 .../dist/css/bootstrap-grid.rtl.min.css.map | 0 .../bootstrap/dist/css/bootstrap-reboot.css | 0 .../dist/css/bootstrap-reboot.css.map | 0 .../dist/css/bootstrap-reboot.min.css | 0 .../dist/css/bootstrap-reboot.min.css.map | 0 .../dist/css/bootstrap-reboot.rtl.css | 0 .../dist/css/bootstrap-reboot.rtl.css.map | 0 .../dist/css/bootstrap-reboot.rtl.min.css | 0 .../dist/css/bootstrap-reboot.rtl.min.css.map | 0 .../dist/css/bootstrap-utilities.css | 0 .../dist/css/bootstrap-utilities.css.map | 0 .../dist/css/bootstrap-utilities.min.css | 0 .../dist/css/bootstrap-utilities.min.css.map | 0 .../dist/css/bootstrap-utilities.rtl.css | 0 .../dist/css/bootstrap-utilities.rtl.css.map | 0 .../dist/css/bootstrap-utilities.rtl.min.css | 0 .../css/bootstrap-utilities.rtl.min.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.css | 0 .../lib/bootstrap/dist/css/bootstrap.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.min.css | 0 .../bootstrap/dist/css/bootstrap.min.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.rtl.css | 0 .../bootstrap/dist/css/bootstrap.rtl.css.map | 0 .../bootstrap/dist/css/bootstrap.rtl.min.css | 0 .../dist/css/bootstrap.rtl.min.css.map | 0 .../lib/bootstrap/dist/js/bootstrap.bundle.js | 0 .../bootstrap/dist/js/bootstrap.bundle.js.map | 0 .../bootstrap/dist/js/bootstrap.bundle.min.js | 0 .../dist/js/bootstrap.bundle.min.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.esm.js | 0 .../bootstrap/dist/js/bootstrap.esm.js.map | 0 .../bootstrap/dist/js/bootstrap.esm.min.js | 0 .../dist/js/bootstrap.esm.min.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.js | 0 .../lib/bootstrap/dist/js/bootstrap.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.min.js | 0 .../bootstrap/dist/js/bootstrap.min.js.map | 0 .../lib/jetbrains-mono/jetbrains-mono.css | 0 .../webfonts/JetBrainsMono-Bold.woff2 | Bin .../webfonts/JetBrainsMono-BoldItalic.woff2 | Bin .../webfonts/JetBrainsMono-ExtraBold.woff2 | Bin .../JetBrainsMono-ExtraBoldItalic.woff2 | Bin .../webfonts/JetBrainsMono-ExtraLight.woff2 | Bin .../JetBrainsMono-ExtraLightItalic.woff2 | Bin .../webfonts/JetBrainsMono-Italic.woff2 | Bin .../webfonts/JetBrainsMono-Light.woff2 | Bin .../webfonts/JetBrainsMono-LightItalic.woff2 | Bin .../webfonts/JetBrainsMono-Medium.woff2 | Bin .../webfonts/JetBrainsMono-MediumItalic.woff2 | Bin .../webfonts/JetBrainsMono-Regular.woff2 | Bin .../webfonts/JetBrainsMono-SemiBold.woff2 | Bin .../JetBrainsMono-SemiBoldItalic.woff2 | Bin .../webfonts/JetBrainsMono-Thin.woff2 | Bin .../webfonts/JetBrainsMono-ThinItalic.woff2 | Bin .../Program.cs | 0 .../Spacebar.AdminApiTest.csproj} | 0 extra/admin-api/flake.lock | Bin 1497 -> 1497 bytes extra/admin-api/flake.nix | 82 +-- extra/admin-api/nuget.config | 9 + extra/admin-api/outputs.nix | 128 +++++ flake.nix | 280 +++++----- 124 files changed, 2250 insertions(+), 268 deletions(-) create mode 100644 extra/admin-api/ConfigTest/ConfigTest.csproj create mode 100644 extra/admin-api/ConfigTest/Program.cs create mode 100644 extra/admin-api/ConfigTest/Properties/launchSettings.json create mode 100644 extra/admin-api/ConfigTest/Worker.cs rename extra/admin-api/{Spacebar.AdminAPI => ConfigTest}/appsettings.Development.json (100%) rename extra/admin-api/{Spacebar.AdminAPI => ConfigTest}/appsettings.json (100%) create mode 100644 extra/admin-api/Spacebar.AdminApi/Controllers/ConfigController.cs rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Controllers/GuildController.cs (99%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Controllers/Media/UserMediaController.cs (91%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Controllers/PingController.cs (90%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Controllers/UserController.cs (99%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Extensions/DbExtensions.cs (89%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Middleware/AuthenticationMiddleware.cs (97%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Program.cs (96%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Properties/launchSettings.json (100%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Services/AuthenticationService.cs (97%) rename extra/admin-api/{Spacebar.AdminAPI => Spacebar.AdminApi}/Services/Configuration.cs (89%) rename extra/admin-api/{Spacebar.AdminAPI/Spacebar.AdminAPI.csproj => Spacebar.AdminApi/Spacebar.AdminApi.csproj} (91%) create mode 100644 extra/admin-api/Spacebar.AdminApi/appsettings.Development.json create mode 100644 extra/admin-api/Spacebar.AdminApi/appsettings.json create mode 100644 extra/admin-api/Spacebar.AdminApi/deps.json create mode 100644 extra/admin-api/Spacebar.CleanSettingsRows/deps.json create mode 100644 extra/admin-api/Spacebar.ConfigModel/Class1.cs create mode 100644 extra/admin-api/Spacebar.ConfigModel/Extensions/JsonExtensions.cs create mode 100644 extra/admin-api/Spacebar.ConfigModel/Spacebar.ConfigModel.csproj create mode 100644 extra/admin-api/Spacebar.Db/deps.json rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/App.razor (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Classes/OpenAPI/OpenAPISchema.cs (99%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Layout/MainLayout.razor (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Layout/MainLayout.razor.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Layout/NavMenu.razor (96%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Layout/NavMenu.razor.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Guilds.razor (98%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Home.razor (98%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/HttpTestClient.razor (97%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor (91%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Login.razor (97%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Media/Index.razor (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Media/Users.razor (98%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/Users.razor (98%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Pages/UsersDelete.razor (98%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Program.cs (95%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Properties/launchSettings.json (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Services/Config.cs (92%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/Services/StreamingHttpClient.cs (97%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj => Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj} (95%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/_Imports.razor (82%) create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/deps.json rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/appsettings.json (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/css/app.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/favicon.png (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/icon-192.png (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/index.html (94%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/jetbrains-mono.css (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPI.TestClient => Spacebar.AdminApi.TestClient}/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPITest => Spacebar.AdminApiTest}/Program.cs (100%) rename extra/admin-api/Utilities/{Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj => Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj} (100%) create mode 100644 extra/admin-api/nuget.config create mode 100644 extra/admin-api/outputs.nix diff --git a/.gitignore b/.gitignore index bb79fbf7e..c1a78e412 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ bun.lock # optional generated outputs from schema.js schemas_orig/ schemas_nested/ -schemas_final/ \ No newline at end of file +schemas_final/ + +!/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/ \ No newline at end of file diff --git a/extra/admin-api/.gitignore b/extra/admin-api/.gitignore index f4e4961b6..65497125b 100644 --- a/extra/admin-api/.gitignore +++ b/extra/admin-api/.gitignore @@ -6,4 +6,4 @@ appsettings.Local*.json /*.patch Spacebar.Db/**/*.orig -Spacebar.Db/**/*.rej +Spacebar.Db/**/*.rej \ No newline at end of file diff --git a/extra/admin-api/ConfigTest/ConfigTest.csproj b/extra/admin-api/ConfigTest/ConfigTest.csproj new file mode 100644 index 000000000..7244b6736 --- /dev/null +++ b/extra/admin-api/ConfigTest/ConfigTest.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + dotnet-ConfigTest-18d89c0e-df5d-447b-8429-7d526a35ab13 + + + + + + + + + + + + + diff --git a/extra/admin-api/ConfigTest/Program.cs b/extra/admin-api/ConfigTest/Program.cs new file mode 100644 index 000000000..5945bcf62 --- /dev/null +++ b/extra/admin-api/ConfigTest/Program.cs @@ -0,0 +1,14 @@ +using ConfigTest; +using Microsoft.EntityFrameworkCore; +using Spacebar.Db.Contexts; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); +builder.Services.AddDbContext(options => { + options + .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) + .EnableDetailedErrors(); +}, ServiceLifetime.Singleton); + +var host = builder.Build(); +host.Run(); diff --git a/extra/admin-api/ConfigTest/Properties/launchSettings.json b/extra/admin-api/ConfigTest/Properties/launchSettings.json new file mode 100644 index 000000000..c355f360c --- /dev/null +++ b/extra/admin-api/ConfigTest/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "ConfigTest": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/extra/admin-api/ConfigTest/Worker.cs b/extra/admin-api/ConfigTest/Worker.cs new file mode 100644 index 000000000..dd3ef7791 --- /dev/null +++ b/extra/admin-api/ConfigTest/Worker.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Nodes; +using Spacebar.ConfigModel.Extensions; +using Spacebar.Db.Contexts; + +namespace ConfigTest; + +public class Worker(ILogger logger, SpacebarDbContext db) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var config = db.Configs + .OrderBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Value); + foreach (var (key, value) in config) + { + Console.WriteLine("Config Key: {0}, Value: {1}", key, value ?? "[NULL]"); + } + + var readConfig = config.ToNestedJsonObject(); + Console.WriteLine(readConfig); + var mapped = readConfig.ToFlatKv(); + foreach (var (key, value) in mapped) + { + Console.WriteLine("Mapped Key: {0}, Value: {1}", key, value ?? "[NULL]"); + } + + // check that they're equal + foreach (var (key, value) in config) + { + if (!mapped.ContainsKey(key)) + { + Console.WriteLine("Missing Key in Mapped: {0}", key); + continue; + } + + if (mapped[key] != value) + { + Console.WriteLine("Value Mismatch for Key: {0}, Original: {1}, Mapped: {2}", key, value ?? "[NULL]", mapped[key] ?? "[NULL]"); + } + } + Environment.Exit(0); + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json b/extra/admin-api/ConfigTest/appsettings.Development.json similarity index 100% rename from extra/admin-api/Spacebar.AdminAPI/appsettings.Development.json rename to extra/admin-api/ConfigTest/appsettings.Development.json diff --git a/extra/admin-api/Spacebar.AdminAPI/appsettings.json b/extra/admin-api/ConfigTest/appsettings.json similarity index 100% rename from extra/admin-api/Spacebar.AdminAPI/appsettings.json rename to extra/admin-api/ConfigTest/appsettings.json diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/ConfigController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/ConfigController.cs new file mode 100644 index 000000000..964656169 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/ConfigController.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.AdminApi.Extensions; +using Spacebar.AdminApi.Models; +using Spacebar.AdminApi.Services; +using Spacebar.Db.Contexts; +using Spacebar.Db.Models; +using Spacebar.RabbitMqUtilities; +using Spacebar.ConfigModel.Extensions; + +namespace Spacebar.AdminApi.Controllers; + +[ApiController] +[Route("/Configuration")] +public class ConfigController(ILogger logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase { + private readonly ILogger _logger = logger; + + [HttpGet] + public async Task Get() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + var config = (await db.Configs.AsNoTracking().ToDictionaryAsync(x => x.Key, x => x.Value)).ToNestedJsonObject(); + return config; + } + + [HttpPost] + public async Task Post([FromBody] JsonObject newConfig) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + var flatConfig = newConfig.ToFlatKv(); + var tasks = flatConfig.Select(async x => { + await using var scope = sp.CreateAsyncScope(); + var scopedDb = scope.ServiceProvider.GetRequiredService(); + var existingConfig = await scopedDb.Configs.FindAsync(x.Key); + if (existingConfig != null) { + existingConfig.Value = x.Value; + scopedDb.Configs.Update(existingConfig); + } + else { + await scopedDb.Configs.AddAsync(new Config + { Key = x.Key, Value = x.Value }); + } + + await scopedDb.SaveChangesAsync(); + }); + await Task.WhenAll(tasks); + // TODO: rabbitmq + + return Ok(); + } + + [HttpPost] + public async Task ReloadConfig() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + // TODO: rabbitmq + + return Ok(); + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs similarity index 99% rename from extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs rename to extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs index 03b38e64d..05bd604aa 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs @@ -2,14 +2,14 @@ using ArcaneLibs.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RabbitMQ.Client; -using Spacebar.AdminAPI.Extensions; +using Spacebar.AdminApi.Extensions; using Spacebar.AdminApi.Models; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; using Spacebar.RabbitMqUtilities; -namespace Spacebar.AdminAPI.Controllers; +namespace Spacebar.AdminApi.Controllers; [ApiController] [Route("/Guilds")] diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/Media/UserMediaController.cs similarity index 91% rename from extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs rename to extra/admin-api/Spacebar.AdminApi/Controllers/Media/UserMediaController.cs index a4d915e5a..f3415731b 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/Media/UserMediaController.cs @@ -1,13 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Spacebar.AdminAPI.Extensions; +using Spacebar.AdminApi.Extensions; using Spacebar.AdminApi.Models; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; using Spacebar.RabbitMqUtilities; -namespace Spacebar.AdminAPI.Controllers.Media; +namespace Spacebar.AdminApi.Controllers.Media; [ApiController] [Route("/media/user")] diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/PingController.cs similarity index 90% rename from extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs rename to extra/admin-api/Spacebar.AdminApi/Controllers/PingController.cs index a2aceb504..05b6d8760 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/PingController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Services; -namespace Spacebar.AdminAPI.Controllers; +namespace Spacebar.AdminApi.Controllers; [ApiController] [Route("/")] diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs similarity index 99% rename from extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs rename to extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs index c727077ab..505f789e1 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs @@ -4,14 +4,14 @@ using ArcaneLibs.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RabbitMQ.Client; -using Spacebar.AdminAPI.Extensions; +using Spacebar.AdminApi.Extensions; using Spacebar.AdminApi.Models; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; using Spacebar.RabbitMqUtilities; -namespace Spacebar.AdminAPI.Controllers; +namespace Spacebar.AdminApi.Controllers; [ApiController] [Route("/users")] diff --git a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs b/extra/admin-api/Spacebar.AdminApi/Extensions/DbExtensions.cs similarity index 89% rename from extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs rename to extra/admin-api/Spacebar.AdminApi/Extensions/DbExtensions.cs index f6318e8f3..1220de913 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs +++ b/extra/admin-api/Spacebar.AdminApi/Extensions/DbExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Spacebar.AdminApi.Models; using Spacebar.Db.Models; -namespace Spacebar.AdminAPI.Extensions; +namespace Spacebar.AdminApi.Extensions; public static class DbExtensions { public static string? GetString(this DbSet config, string key) => config.Find(key)?.Value; diff --git a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs b/extra/admin-api/Spacebar.AdminApi/Middleware/AuthenticationMiddleware.cs similarity index 97% rename from extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs rename to extra/admin-api/Spacebar.AdminApi/Middleware/AuthenticationMiddleware.cs index dea89586c..a5144add5 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs +++ b/extra/admin-api/Spacebar.AdminApi/Middleware/AuthenticationMiddleware.cs @@ -1,11 +1,11 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using Microsoft.IdentityModel.Tokens; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; -namespace Spacebar.AdminAPI.Middleware; +namespace Spacebar.AdminApi.Middleware; public class AuthenticationMiddleware(RequestDelegate next) { private static Dictionary _userCache = new(); diff --git a/extra/admin-api/Spacebar.AdminAPI/Program.cs b/extra/admin-api/Spacebar.AdminApi/Program.cs similarity index 96% rename from extra/admin-api/Spacebar.AdminAPI/Program.cs rename to extra/admin-api/Spacebar.AdminApi/Program.cs index 93b28b15c..f3e977cbe 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Program.cs +++ b/extra/admin-api/Spacebar.AdminApi/Program.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.EntityFrameworkCore; -using Spacebar.AdminAPI.Middleware; -using Spacebar.AdminAPI.Services; +using Spacebar.AdminApi.Middleware; +using Spacebar.AdminApi.Services; using Spacebar.Db.Contexts; using Spacebar.RabbitMqUtilities; diff --git a/extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json b/extra/admin-api/Spacebar.AdminApi/Properties/launchSettings.json similarity index 100% rename from extra/admin-api/Spacebar.AdminAPI/Properties/launchSettings.json rename to extra/admin-api/Spacebar.AdminApi/Properties/launchSettings.json diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs b/extra/admin-api/Spacebar.AdminApi/Services/AuthenticationService.cs similarity index 97% rename from extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs rename to extra/admin-api/Spacebar.AdminApi/Services/AuthenticationService.cs index d402c5b96..788c8eb9a 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs +++ b/extra/admin-api/Spacebar.AdminApi/Services/AuthenticationService.cs @@ -4,7 +4,7 @@ using Microsoft.IdentityModel.Tokens; using Spacebar.Db.Contexts; using Spacebar.Db.Models; -namespace Spacebar.AdminAPI.Services; +namespace Spacebar.AdminApi.Services; public class AuthenticationService(SpacebarDbContext db, Configuration config) { private static Dictionary _userCache = new(); diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs b/extra/admin-api/Spacebar.AdminApi/Services/Configuration.cs similarity index 89% rename from extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs rename to extra/admin-api/Spacebar.AdminApi/Services/Configuration.cs index a58ef5c07..f6b2c0291 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs +++ b/extra/admin-api/Spacebar.AdminApi/Services/Configuration.cs @@ -1,4 +1,4 @@ -namespace Spacebar.AdminAPI.Services; +namespace Spacebar.AdminApi.Services; public class Configuration { public Configuration(IConfiguration configuration) { diff --git a/extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj similarity index 91% rename from extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj rename to extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj index 872e11882..950f8a1f8 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.csproj +++ b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj @@ -16,6 +16,7 @@ + diff --git a/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json b/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json new file mode 100644 index 000000000..032457733 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/appsettings.Development.json @@ -0,0 +1,27 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Trace", //Warning + "Microsoft.AspNetCore.Mvc": "Warning", //Warning + "Microsoft.AspNetCore.HostFiltering": "Warning", //Warning + "Microsoft.AspNetCore.Cors": "Warning", //Warning + // "Microsoft.EntityFrameworkCore": "Warning" + "Microsoft.EntityFrameworkCore.Database.Command": "Debug" + } + }, + "ConnectionStrings": { + "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;", + }, + "RabbitMQ": { + "Host": "127.0.0.1", + "Port": 5673, + "Username": "guest", + "Password": "guest" + }, + "SpacebarAdminApi": { + "Enforce2FA": true, + "OverrideUid": null, + "DisableAuthentication": false + } +} diff --git a/extra/admin-api/Spacebar.AdminApi/appsettings.json b/extra/admin-api/Spacebar.AdminApi/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/extra/admin-api/Spacebar.AdminApi/deps.json b/extra/admin-api/Spacebar.AdminApi/deps.json new file mode 100644 index 000000000..f1ddcb251 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi/deps.json @@ -0,0 +1,447 @@ +[ + { + "pname": "ArcaneLibs", + "version": "1.0.0-preview.20251005-232225", + "hash": "sha256-EsYLSiyX5Nj+ZpFb6FOcAYqDsQFSbvgm9NKaarJjK/0=" + }, + { + "pname": "ArcaneLibs.StringNormalisation", + "version": "1.0.0-preview.20251005-232225", + "hash": "sha256-CdftHRV6idRMojLMgnDRxgSOvfP1DU+9KTKyBGlwoSI=" + }, + { + "pname": "Humanizer.Core", + "version": "2.14.1", + "hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o=" + }, + { + "pname": "Microsoft.AspNetCore.OpenApi", + "version": "10.0.0", + "hash": "sha256-mf3kOdkliLF4OD+CrmEhMNNDGOPKWGBOBWtX2EV5YEU=" + }, + { + "pname": "Microsoft.Build", + "version": "17.7.2", + "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.14.28", + "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.7.2", + "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.14.28", + "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.7.2", + "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.14.28", + "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.7.2", + "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A=" + }, + { + "pname": "Microsoft.CodeAnalysis.Analyzers", + "version": "3.11.0", + "hash": "sha256-hQ2l6E6PO4m7i+ZsfFlEx+93UsLPo4IY3wDkNG11/Sw=" + }, + { + "pname": "Microsoft.CodeAnalysis.Common", + "version": "4.14.0", + "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp", + "version": "4.14.0", + "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp.Workspaces", + "version": "4.14.0", + "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.Common", + "version": "4.14.0", + "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild", + "version": "4.14.0", + "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.0", + "hash": "sha256-xfgrlxhtOkQwF5Q7j8gSm41URJiH8IuJ/T/Dh88++hE=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.1", + "hash": "sha256-IxA+/nDA6hZkUm9bDT6bRu8+9qeHA3gbTvIhR9Ncg4w=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.0", + "hash": "sha256-UDgZbRQcGPaKsE53EH6bvJiv+Q4KSxAbnsVhTVFGG4Q=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.1", + "hash": "sha256-FMGXhAAgcVSedV0/GmUVqAwoiRzFJil9mQYr6eNgowg=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.0", + "hash": "sha256-7Q0jYJO50cqGI+u6gLpootbB8GZvgsgtg0F9FZI1jig=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.1", + "hash": "sha256-O949vr98WoSRXtiIo0ZI6dz2cibIBKbiuC+mQOe9bV4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Design", + "version": "10.0.1", + "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.0", + "hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.1", + "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY=" + }, + { + "pname": "Microsoft.Extensions.Caching.Abstractions", + "version": "10.0.1", + "hash": "sha256-qVLAEqxPK/dNq+z1a6D9NqdcSg/18sfzZhlBWMkqV/A=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.0", + "hash": "sha256-AMgDSm1k6q0s17spGtyR5q8nAqUFDOxl/Fe38f9M+d4=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.1", + "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A=" + }, + { + "pname": "Microsoft.Extensions.Configuration", + "version": "10.0.0", + "hash": "sha256-MsLskVPpkCvov5+DWIaALCt1qfRRX4u228eHxvpE0dg=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "10.0.0", + "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "10.0.1", + "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "10.0.0", + "hash": "sha256-YSiWoA3VQR22k6+bSEAUqeG7UDzZlJfHWDTubUO5V8U=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "10.0.1", + "hash": "sha256-RKOB+zPrtQNUbJY/1jR54rKOM8KHPgynPExxugku3I8=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "9.0.0", + "hash": "sha256-dAH52PPlTLn7X+1aI/7npdrDzMEFPMXRv4isV1a+14k=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.0", + "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.1", + "hash": "sha256-zNUpau51ds7iQTaSUTFtyTHIUoinYc129W50CnufMdQ=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "9.0.0", + "hash": "sha256-CncVwkKZ5CsIG2O0+OM9qXuYXh3p6UGyueTHSLDVL+c=" + }, + { + "pname": "Microsoft.Extensions.DependencyModel", + "version": "10.0.1", + "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.0", + "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.1", + "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "9.0.0", + "hash": "sha256-kR16c+N8nQrWeYLajqnXPg7RiXjZMSFLnKLEs4VfjcM=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.0", + "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.1", + "hash": "sha256-NRk0feNE1fgi/hyO0AVDbSGJQRT+9yte6Lpm4Hz/2Bs=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "9.0.0", + "hash": "sha256-iBTs9twjWXFeERt4CErkIIcoJZU1jrd1RWCI8V5j7KU=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "10.0.1", + "hash": "sha256-vBiSS1vqAC7eDrpJNT4H3A9qLikCSAepnNRbry0mKnk=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "9.0.0", + "hash": "sha256-DT5euAQY/ItB5LPI8WIp6Dnd0lSvBRP35vFkOXC68ck=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.0", + "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.1", + "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "9.0.0", + "hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs=" + }, + { + "pname": "Microsoft.IdentityModel.Abstractions", + "version": "8.15.0", + "hash": "sha256-LKTvERNUTMCEF7xs377tCMwOMRki93OS4kh6Yv0uXJ4=" + }, + { + "pname": "Microsoft.IdentityModel.JsonWebTokens", + "version": "8.15.0", + "hash": "sha256-LwzKiGjcnORvmQ9tim6lomXpfVlPpd/fE8FKTFWKlpM=" + }, + { + "pname": "Microsoft.IdentityModel.Logging", + "version": "8.15.0", + "hash": "sha256-mMXwsjGcrrmHR1mG7BLTKg/30mX+m93QVX17/ynOOd4=" + }, + { + "pname": "Microsoft.IdentityModel.Tokens", + "version": "8.15.0", + "hash": "sha256-7Lo/TsvqgNCEMyFssO3Om233521Pqgb9K9lUeHc5HMk=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.14.28", + "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.7.2", + "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o=" + }, + { + "pname": "Microsoft.OpenApi", + "version": "2.0.0", + "hash": "sha256-8eiM3Mx4Hx1etx52RlczoHG2z9XIpWgu2LQtWSt086k=" + }, + { + "pname": "Mono.TextTemplating", + "version": "3.0.0", + "hash": "sha256-VlgGDvgNZb7MeBbIZ4DE2Nn/j2aD9k6XqNHnASUSDr0=" + }, + { + "pname": "Newtonsoft.Json", + "version": "13.0.3", + "hash": "sha256-hy/BieY4qxBWVVsDqqOPaLy1QobiIapkbrESm6v2PHc=" + }, + { + "pname": "Npgsql", + "version": "10.0.0", + "hash": "sha256-UVKz9dH/rVCCbMyFdqA31RYpht1XgDRLIqUy0Dp9ACQ=" + }, + { + "pname": "Npgsql.EntityFrameworkCore.PostgreSQL", + "version": "10.0.0", + "hash": "sha256-XIJxnTMektQVP1qtslEIGbmBGrIQsvjQjCMRTs9UIbg=" + }, + { + "pname": "RabbitMQ.Client", + "version": "7.2.0", + "hash": "sha256-NkEb2ey0jo/OAeaVOUwIeSbeplqkOsgv1+ys8ZQgemQ=" + }, + { + "pname": "System.CodeDom", + "version": "6.0.0", + "hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ=" + }, + { + "pname": "System.CodeDom", + "version": "7.0.0", + "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE=" + }, + { + "pname": "System.CodeDom", + "version": "9.0.0", + "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw=" + }, + { + "pname": "System.Composition", + "version": "9.0.0", + "hash": "sha256-FehOkQ2u1p8mQ0/wn3cZ+24HjhTLdck8VZYWA1CcgbM=" + }, + { + "pname": "System.Composition.AttributedModel", + "version": "9.0.0", + "hash": "sha256-a7y7H6zj+kmYkllNHA402DoVfY9IaqC3Ooys8Vzl24M=" + }, + { + "pname": "System.Composition.Convention", + "version": "9.0.0", + "hash": "sha256-tw4vE5JRQ60ubTZBbxoMPhtjOQCC3XoDFUH7NHO7o8U=" + }, + { + "pname": "System.Composition.Hosting", + "version": "9.0.0", + "hash": "sha256-oOxU+DPEEfMCuNLgW6wSkZp0JY5gYt44FJNnWt+967s=" + }, + { + "pname": "System.Composition.Runtime", + "version": "9.0.0", + "hash": "sha256-AyIe+di1TqwUBbSJ/sJ8Q8tzsnTN+VBdJw4K8xZz43s=" + }, + { + "pname": "System.Composition.TypedParts", + "version": "9.0.0", + "hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "7.0.0", + "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "9.0.0", + "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "9.0.0", + "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems=" + }, + { + "pname": "System.Formats.Nrbf", + "version": "9.0.0", + "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw=" + }, + { + "pname": "System.IdentityModel.Tokens.Jwt", + "version": "8.15.0", + "hash": "sha256-5O0wbGp0gWnukK+0mWBjMnP1bZc6N0xuNcO2qmFiUX8=" + }, + { + "pname": "System.Reflection.MetadataLoadContext", + "version": "7.0.0", + "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI=" + }, + { + "pname": "System.Resources.Extensions", + "version": "9.0.0", + "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.0", + "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.2", + "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "9.0.0", + "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY=" + }, + { + "pname": "System.Security.Cryptography.ProtectedData", + "version": "9.0.0", + "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "7.0.1", + "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "9.0.0", + "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU=" + }, + { + "pname": "System.Security.Permissions", + "version": "7.0.0", + "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI=" + }, + { + "pname": "System.Security.Permissions", + "version": "9.0.0", + "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0=" + }, + { + "pname": "System.Threading.RateLimiting", + "version": "8.0.0", + "hash": "sha256-KOEWEt6ZthvZHJ2Wp70d9nBhBrPqobGQDi2twlKYh/w=" + }, + { + "pname": "System.Windows.Extensions", + "version": "9.0.0", + "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs=" + }, + { + "pname": "Unidecode.NET", + "version": "2.1.0", + "hash": "sha256-nfxHxG/YwRftOP+wiSW8NL5hIVwt+wnfRb0lURjrZoc=" + } +] diff --git a/extra/admin-api/Spacebar.CleanSettingsRows/deps.json b/extra/admin-api/Spacebar.CleanSettingsRows/deps.json new file mode 100644 index 000000000..2938cdaae --- /dev/null +++ b/extra/admin-api/Spacebar.CleanSettingsRows/deps.json @@ -0,0 +1,492 @@ +[ + { + "pname": "Humanizer.Core", + "version": "2.14.1", + "hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o=" + }, + { + "pname": "Microsoft.Build", + "version": "17.7.2", + "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.14.28", + "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.7.2", + "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.14.28", + "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.7.2", + "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.14.28", + "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.7.2", + "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A=" + }, + { + "pname": "Microsoft.CodeAnalysis.Analyzers", + "version": "3.11.0", + "hash": "sha256-hQ2l6E6PO4m7i+ZsfFlEx+93UsLPo4IY3wDkNG11/Sw=" + }, + { + "pname": "Microsoft.CodeAnalysis.Common", + "version": "4.14.0", + "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp", + "version": "4.14.0", + "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp.Workspaces", + "version": "4.14.0", + "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.Common", + "version": "4.14.0", + "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild", + "version": "4.14.0", + "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.0", + "hash": "sha256-xfgrlxhtOkQwF5Q7j8gSm41URJiH8IuJ/T/Dh88++hE=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.1", + "hash": "sha256-IxA+/nDA6hZkUm9bDT6bRu8+9qeHA3gbTvIhR9Ncg4w=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.0", + "hash": "sha256-UDgZbRQcGPaKsE53EH6bvJiv+Q4KSxAbnsVhTVFGG4Q=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.1", + "hash": "sha256-FMGXhAAgcVSedV0/GmUVqAwoiRzFJil9mQYr6eNgowg=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.0", + "hash": "sha256-7Q0jYJO50cqGI+u6gLpootbB8GZvgsgtg0F9FZI1jig=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.1", + "hash": "sha256-O949vr98WoSRXtiIo0ZI6dz2cibIBKbiuC+mQOe9bV4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Design", + "version": "10.0.1", + "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.0", + "hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.1", + "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY=" + }, + { + "pname": "Microsoft.Extensions.Caching.Abstractions", + "version": "10.0.1", + "hash": "sha256-qVLAEqxPK/dNq+z1a6D9NqdcSg/18sfzZhlBWMkqV/A=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.0", + "hash": "sha256-AMgDSm1k6q0s17spGtyR5q8nAqUFDOxl/Fe38f9M+d4=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.1", + "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A=" + }, + { + "pname": "Microsoft.Extensions.Configuration", + "version": "10.0.0", + "hash": "sha256-MsLskVPpkCvov5+DWIaALCt1qfRRX4u228eHxvpE0dg=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "10.0.0", + "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "10.0.1", + "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "10.0.0", + "hash": "sha256-YSiWoA3VQR22k6+bSEAUqeG7UDzZlJfHWDTubUO5V8U=" + }, + { + "pname": "Microsoft.Extensions.Configuration.CommandLine", + "version": "10.0.0", + "hash": "sha256-ldTiRFqnv8/pA0gl6UR+4DDGAIZOf9+MhaLWOuKOXOI=" + }, + { + "pname": "Microsoft.Extensions.Configuration.EnvironmentVariables", + "version": "10.0.0", + "hash": "sha256-UayfeqrAmNyfOkuhcBKfj8UpjQqV/ZMqWrDyxCSG1MA=" + }, + { + "pname": "Microsoft.Extensions.Configuration.FileExtensions", + "version": "10.0.0", + "hash": "sha256-rN+3rqrHiTaBfHgP+E4dA8Qm2cFJPfbEcd93yKLsqlQ=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Json", + "version": "10.0.0", + "hash": "sha256-VCFukgsxiQ2MFGE6RDMFTGopBHbcZL2t0ER7ENaFXRY=" + }, + { + "pname": "Microsoft.Extensions.Configuration.UserSecrets", + "version": "10.0.0", + "hash": "sha256-uIoIpbDPRMfFqT8Y6j/wHbFCAly6H1N9qpxnomRbHIo=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "10.0.0", + "hash": "sha256-LYm9hVlo/R9c2aAKHsDYJ5vY9U0+3Jvclme3ou3BtvQ=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "10.0.1", + "hash": "sha256-RKOB+zPrtQNUbJY/1jR54rKOM8KHPgynPExxugku3I8=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "9.0.0", + "hash": "sha256-dAH52PPlTLn7X+1aI/7npdrDzMEFPMXRv4isV1a+14k=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.0", + "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.1", + "hash": "sha256-zNUpau51ds7iQTaSUTFtyTHIUoinYc129W50CnufMdQ=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "9.0.0", + "hash": "sha256-CncVwkKZ5CsIG2O0+OM9qXuYXh3p6UGyueTHSLDVL+c=" + }, + { + "pname": "Microsoft.Extensions.DependencyModel", + "version": "10.0.1", + "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics", + "version": "10.0.0", + "hash": "sha256-o7QkCisEcFIh227qBUfWFci2ns4cgEpLqpX7YvHGToQ=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics.Abstractions", + "version": "10.0.0", + "hash": "sha256-cix7QxQ/g3sj6reXu3jn0cRv2RijzceaLLkchEGTt5E=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Abstractions", + "version": "10.0.0", + "hash": "sha256-CHDs2HCN8QcfuYQpgNVszZ5dfXFe4yS9K2GoQXecc20=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Physical", + "version": "10.0.0", + "hash": "sha256-2Rw/cwBO+/A3QY2IjN/c8Y0LhtC1qTBL7VdJiD1J2UQ=" + }, + { + "pname": "Microsoft.Extensions.FileSystemGlobbing", + "version": "10.0.0", + "hash": "sha256-ETfVTdsdBtp69EggLg/AARTQW4lLQYVdVldXIQrsjZA=" + }, + { + "pname": "Microsoft.Extensions.Hosting", + "version": "10.0.0", + "hash": "sha256-tY0g6lCy2yFprE+NmriiU6FGmwpzxV8LqE0ZFNKIwuM=" + }, + { + "pname": "Microsoft.Extensions.Hosting.Abstractions", + "version": "10.0.0", + "hash": "sha256-Sub3Thay/+eR84cEODk/nPh1oYIYtawvDX6r0duReqo=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.0", + "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.1", + "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "9.0.0", + "hash": "sha256-kR16c+N8nQrWeYLajqnXPg7RiXjZMSFLnKLEs4VfjcM=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.0", + "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.1", + "hash": "sha256-NRk0feNE1fgi/hyO0AVDbSGJQRT+9yte6Lpm4Hz/2Bs=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "9.0.0", + "hash": "sha256-iBTs9twjWXFeERt4CErkIIcoJZU1jrd1RWCI8V5j7KU=" + }, + { + "pname": "Microsoft.Extensions.Logging.Configuration", + "version": "10.0.0", + "hash": "sha256-7/TWO1aq8hdgbcTEKDBWIjgSC9KpFN3kRnMX+12bOkU=" + }, + { + "pname": "Microsoft.Extensions.Logging.Console", + "version": "10.0.0", + "hash": "sha256-Rsblo7GSMTOr43876KkdvqS6wU9Typ1yoFK3tL50CBk=" + }, + { + "pname": "Microsoft.Extensions.Logging.Debug", + "version": "10.0.0", + "hash": "sha256-n/+KRVlsgKm17cJImaoAPHAObHpApW/hf6mMsQFGrvY=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventLog", + "version": "10.0.0", + "hash": "sha256-4RJ2r80RtI3QUAhCAYbGnA0YcTmouqtZvQU9o3CrB38=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventSource", + "version": "10.0.0", + "hash": "sha256-tqC13Qwkf4x14iGxOYlXTyeoN8KPVX+mupv2LdpzGHo=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "10.0.0", + "hash": "sha256-j5MOqZSKeUtxxzmZjzZMGy0vELHdvPraqwTQQQNVsYA=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "10.0.1", + "hash": "sha256-vBiSS1vqAC7eDrpJNT4H3A9qLikCSAepnNRbry0mKnk=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "9.0.0", + "hash": "sha256-DT5euAQY/ItB5LPI8WIp6Dnd0lSvBRP35vFkOXC68ck=" + }, + { + "pname": "Microsoft.Extensions.Options.ConfigurationExtensions", + "version": "10.0.0", + "hash": "sha256-XGAs5DxMvWnmjX8dqRwKY0vsuS40SHvsfJqB1rO4L7k=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.0", + "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.1", + "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "9.0.0", + "hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.14.28", + "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.7.2", + "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o=" + }, + { + "pname": "Mono.TextTemplating", + "version": "3.0.0", + "hash": "sha256-VlgGDvgNZb7MeBbIZ4DE2Nn/j2aD9k6XqNHnASUSDr0=" + }, + { + "pname": "Newtonsoft.Json", + "version": "13.0.3", + "hash": "sha256-hy/BieY4qxBWVVsDqqOPaLy1QobiIapkbrESm6v2PHc=" + }, + { + "pname": "Npgsql", + "version": "10.0.0", + "hash": "sha256-UVKz9dH/rVCCbMyFdqA31RYpht1XgDRLIqUy0Dp9ACQ=" + }, + { + "pname": "Npgsql.EntityFrameworkCore.PostgreSQL", + "version": "10.0.0", + "hash": "sha256-XIJxnTMektQVP1qtslEIGbmBGrIQsvjQjCMRTs9UIbg=" + }, + { + "pname": "System.CodeDom", + "version": "6.0.0", + "hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ=" + }, + { + "pname": "System.CodeDom", + "version": "7.0.0", + "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE=" + }, + { + "pname": "System.CodeDom", + "version": "9.0.0", + "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw=" + }, + { + "pname": "System.Composition", + "version": "9.0.0", + "hash": "sha256-FehOkQ2u1p8mQ0/wn3cZ+24HjhTLdck8VZYWA1CcgbM=" + }, + { + "pname": "System.Composition.AttributedModel", + "version": "9.0.0", + "hash": "sha256-a7y7H6zj+kmYkllNHA402DoVfY9IaqC3Ooys8Vzl24M=" + }, + { + "pname": "System.Composition.Convention", + "version": "9.0.0", + "hash": "sha256-tw4vE5JRQ60ubTZBbxoMPhtjOQCC3XoDFUH7NHO7o8U=" + }, + { + "pname": "System.Composition.Hosting", + "version": "9.0.0", + "hash": "sha256-oOxU+DPEEfMCuNLgW6wSkZp0JY5gYt44FJNnWt+967s=" + }, + { + "pname": "System.Composition.Runtime", + "version": "9.0.0", + "hash": "sha256-AyIe+di1TqwUBbSJ/sJ8Q8tzsnTN+VBdJw4K8xZz43s=" + }, + { + "pname": "System.Composition.TypedParts", + "version": "9.0.0", + "hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "7.0.0", + "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "9.0.0", + "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "10.0.0", + "hash": "sha256-pN3tld926Fp0n5ZNjjzIJviUQrynlOAB0vhc1aoso6E=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "9.0.0", + "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems=" + }, + { + "pname": "System.Formats.Nrbf", + "version": "9.0.0", + "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw=" + }, + { + "pname": "System.Reflection.MetadataLoadContext", + "version": "7.0.0", + "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI=" + }, + { + "pname": "System.Resources.Extensions", + "version": "9.0.0", + "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.0", + "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.2", + "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "9.0.0", + "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY=" + }, + { + "pname": "System.Security.Cryptography.ProtectedData", + "version": "9.0.0", + "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "7.0.1", + "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "9.0.0", + "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU=" + }, + { + "pname": "System.Security.Permissions", + "version": "7.0.0", + "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI=" + }, + { + "pname": "System.Security.Permissions", + "version": "9.0.0", + "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0=" + }, + { + "pname": "System.Windows.Extensions", + "version": "9.0.0", + "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs=" + } +] diff --git a/extra/admin-api/Spacebar.ConfigModel/Class1.cs b/extra/admin-api/Spacebar.ConfigModel/Class1.cs new file mode 100644 index 000000000..948ca91a2 --- /dev/null +++ b/extra/admin-api/Spacebar.ConfigModel/Class1.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.ConfigModel; + +public class Config +{ + [JsonPropertyName("admin")] public EndpointConfig Admin { get; set; } = null!; + [JsonPropertyName("api")] public EndpointConfig Api { get; set; } = null!; + [JsonPropertyName("gateway")] public EndpointConfig Gateway { get; set; } = null!; + [JsonPropertyName("cdn")] public EndpointConfig Cdn { get; set; } = null!; + + public Config ReadFromKv(Dictionary kv) + { + // to object + + + foreach (var (key, value) in kv) + { + switch (key.Split('_', 2)[0]) + { + default: + Console.WriteLine($"Unrecognized config key prefix: {key}"); + continue; + } + } + + return this; + } +} + +public class EndpointConfig +{ + [JsonPropertyName("endpointPrivate")] public string? EndpointPrivate { get; set; } + [JsonPropertyName("endpointPublic")] public string? EndpointPublic { get; set; } + + public EndpointConfig ReadFromKv(Dictionary kv, string prefix) + { + foreach (var (key, value) in kv) + { + if (!key.StartsWith(prefix + "_")) continue; + var subKey = key[(prefix + "_").Length..]; + switch (subKey) + { + case "ENDPOINT_PRIVATE": + EndpointPrivate = value?.ToString(); + break; + case "ENDPOINT_PUBLIC": + EndpointPublic = value?.ToString(); + break; + default: + Console.WriteLine($"Unrecognized config key: {key}"); + break; + } + } + + return this; + } +} + +public class ApiConfig : EndpointConfig +{ + [JsonPropertyName("activeVersions")] public List ActiveVersions { get; set; } = null!; + [JsonPropertyName("defaultVersion")] public string DefaultVersion { get; set; } = null!; +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.ConfigModel/Extensions/JsonExtensions.cs b/extra/admin-api/Spacebar.ConfigModel/Extensions/JsonExtensions.cs new file mode 100644 index 000000000..20ae73318 --- /dev/null +++ b/extra/admin-api/Spacebar.ConfigModel/Extensions/JsonExtensions.cs @@ -0,0 +1,119 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Spacebar.ConfigModel.Extensions; + +public static class JsonExtensions +{ + extension(Dictionary kv) + { + public JsonObject ToNestedJsonObject(string path = "$") + { + JsonObject root = new(); + // group by prefix + var groups = kv.GroupBy(kvItem => kvItem.Key.Split('_', 2)[0]); + foreach (var group in groups) + { + var prefix = group.Key; + + if (group.Count() == 1 && !group.First().Key.Contains('_')) + { + root[prefix] = group.First().Value == null ? null : JsonNode.Parse(group.First().Value!); + Console.WriteLine("[CONFIG] Single Key: {0}.{1}, Value: {2}", path, prefix, root[prefix]?.ToJsonString()); + continue; + } + + var nestedValues = group.Where(x => x.Key.Contains('_')).ToDictionary(kvItem => kvItem.Key[(prefix.Length + 1)..], kvItem => kvItem.Value); + + if (nestedValues.All(x => int.TryParse(x.Key.Split('_')[0], out _))) + { + Console.WriteLine("[CONFIG] Array Key Detected: {0}.{1}", path, prefix); + var arr = new JsonArray(); + if (nestedValues.All(x => x.Key.Contains('_'))) + { + var objs = nestedValues.GroupBy(x => x.Key.Split('_', 2)[0]); + foreach (var objGroup in objs.OrderBy(x => int.Parse(x.Key))) + { + var i = objGroup.Key; + var objValues = objGroup.ToDictionary(kvItem => kvItem.Key[(i.Length + 1)..], kvItem => kvItem.Value); + var obj = objValues.ToNestedJsonObject($"{path}.{prefix}[{i}]"); + arr.Add(obj); + Console.WriteLine($" - ${path}.{prefix}[{i}]: {obj.ToJsonString()}"); + } + } + else + foreach (var (i, arrayItem) in nestedValues.OrderBy(x => int.Parse(x.Key))) + { + arr.Add(arrayItem == null ? null : JsonNode.Parse(arrayItem)); + Console.WriteLine($" - {path}.{prefix}[{i}]: {arrayItem}"); + } + + root[prefix] = arr; + } + else + { + root[prefix] = nestedValues.ToNestedJsonObject($"{path}.{prefix}"); + } + } + + return root; + } + } + + extension(JsonObject jo) + { + public Dictionary ToFlatKv(string path = "$") + { + var kv = new Dictionary(); + var jso = new JsonSerializerOptions() + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + foreach (var (key, value) in jo) + { + var currentPath = path == "$" ? key : $"{path}_{key}"; + + switch (value) + { + case JsonObject nestedObj: + var nestedKv = nestedObj.ToFlatKv(currentPath); + foreach (var (nestedKey, nestedValue) in nestedKv) + { + kv[nestedKey] = nestedValue; + } + + break; + case JsonArray arr: + for (int i = 0; i < arr.Count; i++) + { + var item = arr[i]; + var itemPath = $"{currentPath}_{i}"; + switch (item) + { + case JsonObject arrObj: + var arrObjKv = arrObj.ToFlatKv(itemPath); + foreach (var (arrObjKey, arrObjValue) in arrObjKv) + { + kv[arrObjKey] = arrObjValue; + } + + break; + default: + kv[itemPath] = item?.ToJsonString(jso); + break; + } + } + + break; + default: + Console.WriteLine(value?.GetType()); + kv[currentPath] = value?.ToJsonString(jso); + break; + } + } + + return kv; + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.ConfigModel/Spacebar.ConfigModel.csproj b/extra/admin-api/Spacebar.ConfigModel/Spacebar.ConfigModel.csproj new file mode 100644 index 000000000..237d66167 --- /dev/null +++ b/extra/admin-api/Spacebar.ConfigModel/Spacebar.ConfigModel.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/extra/admin-api/Spacebar.Db/deps.json b/extra/admin-api/Spacebar.Db/deps.json new file mode 100644 index 000000000..c978dd1b7 --- /dev/null +++ b/extra/admin-api/Spacebar.Db/deps.json @@ -0,0 +1,362 @@ +[ + { + "pname": "Humanizer.Core", + "version": "2.14.1", + "hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o=" + }, + { + "pname": "Microsoft.Build", + "version": "17.7.2", + "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.14.28", + "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM=" + }, + { + "pname": "Microsoft.Build.Framework", + "version": "17.7.2", + "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.14.28", + "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0=" + }, + { + "pname": "Microsoft.Build.Tasks.Core", + "version": "17.7.2", + "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.14.28", + "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU=" + }, + { + "pname": "Microsoft.Build.Utilities.Core", + "version": "17.7.2", + "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A=" + }, + { + "pname": "Microsoft.CodeAnalysis.Analyzers", + "version": "3.11.0", + "hash": "sha256-hQ2l6E6PO4m7i+ZsfFlEx+93UsLPo4IY3wDkNG11/Sw=" + }, + { + "pname": "Microsoft.CodeAnalysis.Common", + "version": "4.14.0", + "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp", + "version": "4.14.0", + "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY=" + }, + { + "pname": "Microsoft.CodeAnalysis.CSharp.Workspaces", + "version": "4.14.0", + "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.Common", + "version": "4.14.0", + "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I=" + }, + { + "pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild", + "version": "4.14.0", + "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.0", + "hash": "sha256-xfgrlxhtOkQwF5Q7j8gSm41URJiH8IuJ/T/Dh88++hE=" + }, + { + "pname": "Microsoft.EntityFrameworkCore", + "version": "10.0.1", + "hash": "sha256-IxA+/nDA6hZkUm9bDT6bRu8+9qeHA3gbTvIhR9Ncg4w=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.0", + "hash": "sha256-UDgZbRQcGPaKsE53EH6bvJiv+Q4KSxAbnsVhTVFGG4Q=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Abstractions", + "version": "10.0.1", + "hash": "sha256-FMGXhAAgcVSedV0/GmUVqAwoiRzFJil9mQYr6eNgowg=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.0", + "hash": "sha256-7Q0jYJO50cqGI+u6gLpootbB8GZvgsgtg0F9FZI1jig=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Analyzers", + "version": "10.0.1", + "hash": "sha256-O949vr98WoSRXtiIo0ZI6dz2cibIBKbiuC+mQOe9bV4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Design", + "version": "10.0.1", + "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.0", + "hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4=" + }, + { + "pname": "Microsoft.EntityFrameworkCore.Relational", + "version": "10.0.1", + "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY=" + }, + { + "pname": "Microsoft.Extensions.Caching.Abstractions", + "version": "10.0.1", + "hash": "sha256-qVLAEqxPK/dNq+z1a6D9NqdcSg/18sfzZhlBWMkqV/A=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.0", + "hash": "sha256-AMgDSm1k6q0s17spGtyR5q8nAqUFDOxl/Fe38f9M+d4=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "10.0.1", + "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "10.0.1", + "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "10.0.1", + "hash": "sha256-RKOB+zPrtQNUbJY/1jR54rKOM8KHPgynPExxugku3I8=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "9.0.0", + "hash": "sha256-dAH52PPlTLn7X+1aI/7npdrDzMEFPMXRv4isV1a+14k=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.1", + "hash": "sha256-zNUpau51ds7iQTaSUTFtyTHIUoinYc129W50CnufMdQ=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "9.0.0", + "hash": "sha256-CncVwkKZ5CsIG2O0+OM9qXuYXh3p6UGyueTHSLDVL+c=" + }, + { + "pname": "Microsoft.Extensions.DependencyModel", + "version": "10.0.1", + "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.0", + "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "10.0.1", + "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "9.0.0", + "hash": "sha256-kR16c+N8nQrWeYLajqnXPg7RiXjZMSFLnKLEs4VfjcM=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.0", + "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.1", + "hash": "sha256-NRk0feNE1fgi/hyO0AVDbSGJQRT+9yte6Lpm4Hz/2Bs=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "9.0.0", + "hash": "sha256-iBTs9twjWXFeERt4CErkIIcoJZU1jrd1RWCI8V5j7KU=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "10.0.1", + "hash": "sha256-vBiSS1vqAC7eDrpJNT4H3A9qLikCSAepnNRbry0mKnk=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "9.0.0", + "hash": "sha256-DT5euAQY/ItB5LPI8WIp6Dnd0lSvBRP35vFkOXC68ck=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.1", + "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "9.0.0", + "hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.14.28", + "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI=" + }, + { + "pname": "Microsoft.NET.StringTools", + "version": "17.7.2", + "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o=" + }, + { + "pname": "Mono.TextTemplating", + "version": "3.0.0", + "hash": "sha256-VlgGDvgNZb7MeBbIZ4DE2Nn/j2aD9k6XqNHnASUSDr0=" + }, + { + "pname": "Newtonsoft.Json", + "version": "13.0.3", + "hash": "sha256-hy/BieY4qxBWVVsDqqOPaLy1QobiIapkbrESm6v2PHc=" + }, + { + "pname": "Npgsql", + "version": "10.0.0", + "hash": "sha256-UVKz9dH/rVCCbMyFdqA31RYpht1XgDRLIqUy0Dp9ACQ=" + }, + { + "pname": "Npgsql.EntityFrameworkCore.PostgreSQL", + "version": "10.0.0", + "hash": "sha256-XIJxnTMektQVP1qtslEIGbmBGrIQsvjQjCMRTs9UIbg=" + }, + { + "pname": "System.CodeDom", + "version": "6.0.0", + "hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ=" + }, + { + "pname": "System.CodeDom", + "version": "7.0.0", + "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE=" + }, + { + "pname": "System.CodeDom", + "version": "9.0.0", + "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw=" + }, + { + "pname": "System.Composition", + "version": "9.0.0", + "hash": "sha256-FehOkQ2u1p8mQ0/wn3cZ+24HjhTLdck8VZYWA1CcgbM=" + }, + { + "pname": "System.Composition.AttributedModel", + "version": "9.0.0", + "hash": "sha256-a7y7H6zj+kmYkllNHA402DoVfY9IaqC3Ooys8Vzl24M=" + }, + { + "pname": "System.Composition.Convention", + "version": "9.0.0", + "hash": "sha256-tw4vE5JRQ60ubTZBbxoMPhtjOQCC3XoDFUH7NHO7o8U=" + }, + { + "pname": "System.Composition.Hosting", + "version": "9.0.0", + "hash": "sha256-oOxU+DPEEfMCuNLgW6wSkZp0JY5gYt44FJNnWt+967s=" + }, + { + "pname": "System.Composition.Runtime", + "version": "9.0.0", + "hash": "sha256-AyIe+di1TqwUBbSJ/sJ8Q8tzsnTN+VBdJw4K8xZz43s=" + }, + { + "pname": "System.Composition.TypedParts", + "version": "9.0.0", + "hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "7.0.0", + "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "9.0.0", + "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "9.0.0", + "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems=" + }, + { + "pname": "System.Formats.Nrbf", + "version": "9.0.0", + "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw=" + }, + { + "pname": "System.Reflection.MetadataLoadContext", + "version": "7.0.0", + "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI=" + }, + { + "pname": "System.Resources.Extensions", + "version": "9.0.0", + "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.0", + "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "7.0.2", + "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8=" + }, + { + "pname": "System.Security.Cryptography.Pkcs", + "version": "9.0.0", + "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY=" + }, + { + "pname": "System.Security.Cryptography.ProtectedData", + "version": "9.0.0", + "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "7.0.1", + "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w=" + }, + { + "pname": "System.Security.Cryptography.Xml", + "version": "9.0.0", + "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU=" + }, + { + "pname": "System.Security.Permissions", + "version": "7.0.0", + "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI=" + }, + { + "pname": "System.Security.Permissions", + "version": "9.0.0", + "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0=" + }, + { + "pname": "System.Windows.Extensions", + "version": "9.0.0", + "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs=" + } +] diff --git a/extra/admin-api/SpacebarAdminAPI.sln b/extra/admin-api/SpacebarAdminAPI.sln index 09e3a6969..723939d00 100644 --- a/extra/admin-api/SpacebarAdminAPI.sln +++ b/extra/admin-api/SpacebarAdminAPI.sln @@ -5,15 +5,15 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Db", "Spacebar.Db\Spacebar.Db.csproj", "{524849DF-93BC-4632-B6C2-D05552C13887}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminAPI", "Spacebar.AdminAPI\Spacebar.AdminAPI.csproj", "{00E58C53-0AC1-4113-8CCF-D299861EA8D3}" +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}" +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}" +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 @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.AdminApi.PrepareTe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.CleanSettingsRows", "Spacebar.CleanSettingsRows\Spacebar.CleanSettingsRows.csproj", "{9B41FAC1-4427-487C-BF3F-69554848DEBF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.ConfigModel", "Spacebar.ConfigModel\Spacebar.ConfigModel.csproj", "{EC5C6A71-458A-4A1D-BE96-CCB84FF4AF1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigTest", "ConfigTest\ConfigTest.csproj", "{3BB51E17-8D3E-42FE-AAE6-C33DA5D8D323}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +66,14 @@ Global {9B41FAC1-4427-487C-BF3F-69554848DEBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B41FAC1-4427-487C-BF3F-69554848DEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B41FAC1-4427-487C-BF3F-69554848DEBF}.Release|Any CPU.Build.0 = Release|Any CPU + {EC5C6A71-458A-4A1D-BE96-CCB84FF4AF1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC5C6A71-458A-4A1D-BE96-CCB84FF4AF1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC5C6A71-458A-4A1D-BE96-CCB84FF4AF1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC5C6A71-458A-4A1D-BE96-CCB84FF4AF1B}.Release|Any CPU.Build.0 = Release|Any CPU + {3BB51E17-8D3E-42FE-AAE6-C33DA5D8D323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BB51E17-8D3E-42FE-AAE6-C33DA5D8D323}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BB51E17-8D3E-42FE-AAE6-C33DA5D8D323}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BB51E17-8D3E-42FE-AAE6-C33DA5D8D323}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4E179D1-A3EB-4A1D-9C11-E7019F4B1EE2} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99} diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/App.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/App.razor similarity index 100% rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/App.razor rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/App.razor diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Classes/OpenAPI/OpenAPISchema.cs similarity index 99% rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Classes/OpenAPI/OpenAPISchema.cs index 8e399302e..8995dee5f 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Classes/OpenAPI/OpenAPISchema.cs @@ -2,7 +2,7 @@ using System.Collections.Frozen; using System.Text.Json; using System.Text.Json.Serialization; -namespace Spacebar.AdminAPI.TestClient.Classes.OpenAPI; +namespace Spacebar.AdminApi.TestClient.Classes.OpenAPI; public class OpenApiSchema { [JsonPropertyName("openapi")] diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/MainLayout.razor similarity index 100% rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/MainLayout.razor diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/MainLayout.razor.css similarity index 100% rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor.css rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/MainLayout.razor.css diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/NavMenu.razor similarity index 96% rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/NavMenu.razor index c96fa30c4..96deb070b 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/NavMenu.razor @@ -1,6 +1,6 @@