Compare commits

..

2 Commits

Author SHA1 Message Date
a37fd81052
create account command
update packages
2023-05-08 22:07:29 +03:00
26175ce357
runtime flags & rabbitmq 2023-05-08 20:47:10 +03:00
11 changed files with 376 additions and 19 deletions

View File

@ -1,6 +1,7 @@
import { LoggerMasterOptions, LoggerClientOptions } from '@navy.gif/logger'; import { LoggerMasterOptions, LoggerClientOptions } from '@navy.gif/logger';
import { ServerOptions } from './Server.js'; import { ServerOptions } from './Server.js';
import { DatabaseOptions, DiscordOptions } from './Other.js'; import { DatabaseOptions, DiscordOptions } from './Other.js';
import { BrokerOptions } from '@navy.gif/wrappers';
type Env = { type Env = {
[key: string]: string [key: string]: string
@ -25,5 +26,6 @@ export type ControllerOptions = {
discord: DiscordOptions, discord: DiscordOptions,
databases: DatabaseOptions, databases: DatabaseOptions,
env: Env, env: Env,
srcDir: string srcDir: string,
rabbitConfig: BrokerOptions
} }

11
@types/Flags.ts Normal file
View File

@ -0,0 +1,11 @@
export type FlagType = string | number | boolean | number[] | null
export type FlagEnv = 'test' | 'prod'
export type FlagConsumer = 'client' | 'server' | 'api'
export type FlagData<T = FlagType> = {
_id?: string,
name: string,
env: FlagEnv,
consumer: FlagConsumer,
value: T
}

View File

@ -1,6 +1,6 @@
import { AbstractUser } from "../src/server/interfaces/index.js"; import { AbstractUser } from "../src/server/interfaces/index.js";
import { LoggerClientOptions } from '@navy.gif/logger'; import { LoggerClientOptions } from '@navy.gif/logger';
import { MariaOptions, MongoOptions, ObjectId } from '@navy.gif/wrappers'; import { BrokerOptions, MariaOptions, MongoOptions, ObjectId } from '@navy.gif/wrappers';
import { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from "express"; import { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from "express";
import http from 'http'; import http from 'http';
import { UploadedFile } from "express-fileupload"; import { UploadedFile } from "express-fileupload";
@ -51,7 +51,8 @@ export type ServerOptions = {
discord: { discord: {
scope?: string[], scope?: string[],
version?: number version?: number
} },
rabbitConfig: BrokerOptions
} }
export type Permissions = { export type Permissions = {

View File

@ -15,7 +15,7 @@
"@navy.gif/commandparser": "^1.4.5", "@navy.gif/commandparser": "^1.4.5",
"@navy.gif/logger": "^2.3.3", "@navy.gif/logger": "^2.3.3",
"@navy.gif/passport-discord": "^0.2.2-b", "@navy.gif/passport-discord": "^0.2.2-b",
"@navy.gif/wrappers": "^1.3.13", "@navy.gif/wrappers": "^1.3.14",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/express-fileupload": "^1.4.1", "@types/express-fileupload": "^1.4.1",
"@types/express-session": "^1.17.7", "@types/express-session": "^1.17.7",

View File

@ -18,6 +18,7 @@ import { ControllerOptions } from '../../@types/Controller.js';
import BaseCommand from './BaseCommand.js'; import BaseCommand from './BaseCommand.js';
import { IPCMessage } from '../../@types/Other.js'; import { IPCMessage } from '../../@types/Other.js';
import { ServerOptions } from '../../@types/Server.js'; import { ServerOptions } from '../../@types/Server.js';
import { BrokerOptions } from '@navy.gif/wrappers';
class Controller extends EventEmitter { class Controller extends EventEmitter {
@ -97,7 +98,8 @@ class Controller extends EventEmitter {
serverOptions = {} as ServerOptions, serverOptions = {} as ServerOptions,
logger = {}, logger = {},
discord = {}, discord = {},
databases = {} databases = {},
rabbitConfig = {} as BrokerOptions
} = this.#_options; } = this.#_options;
this.#_logger.info(`Spawning ${shardCount} shards`); this.#_logger.info(`Spawning ${shardCount} shards`);
@ -129,7 +131,7 @@ class Controller extends EventEmitter {
for (let i = 0; i < shardCount; i++) { for (let i = 0; i < shardCount; i++) {
const shard = new Shard(this, i, { const shard = new Shard(this, i, {
serverOptions: { serverOptions: {
...serverOptions, logger, discord, databases ...serverOptions, logger, discord, databases, rabbitConfig
}, },
...shardOptions, ...shardOptions,
env: this.#_options.env, env: this.#_options.env,

View File

@ -2,6 +2,7 @@ import { OptionType, ArgsResult } from '@navy.gif/commandparser';
import BaseCommand from '../BaseCommand.js'; import BaseCommand from '../BaseCommand.js';
import Controller from '../Controller.js'; import Controller from '../Controller.js';
import { IPCMessage, SignupCode } from '../../../@types/Other.js'; import { IPCMessage, SignupCode } from '../../../@types/Other.js';
import Util from '../../util/Util.js';
class CreateCommand extends BaseCommand { class CreateCommand extends BaseCommand {
@ -19,20 +20,62 @@ class CreateCommand extends BaseCommand {
valueOptional: true, valueOptional: true,
defaultValue: 1 defaultValue: 1
}] }]
}, {
name: 'account',
type: OptionType.SUB_COMMAND,
options: [{
name: 'name',
type: OptionType.STRING,
}, {
name: 'password',
type: OptionType.STRING
}]
}] }]
}); });
} }
async execute ({ subcommand, args }: {subcommand: string, args: ArgsResult}) { async execute ({ subcommand, args }: {subcommand: string, args: ArgsResult}) {
if (subcommand === 'registration-code')
return this.#code(args);
else if (subcommand === 'account')
return this.#account(args);
return 'Unknown subcommand';
}
async #account (args: ArgsResult) {
const { name, password } = args;
let accountName = 'admin';
let accountPass = Util.randomString();
if (name)
accountName = name.value as string;
if (password)
accountPass = password.value as string;
const shard = this.controller.shards.random();
if (!shard)
return 'No available shard';
const msg = {
type: 'account-create',
accountName,
accountPass
};
const response = await shard.send(msg, true) as IPCMessage;
if (response.success)
return `Account ${accountName} created with password ${accountPass}`;
return `Failed to create account:\n${response.message}`;
}
async #code (args: ArgsResult) {
const amount = args.amount?.value || 1; const amount = args.amount?.value || 1;
const shard = this.controller.shards.random(); const shard = this.controller.shards.random();
if (!shard) if (!shard)
return `No available shard`; return `No available shard`;
const msg: IPCMessage = { amount }; const msg: IPCMessage = { amount, type: 'reqregcode' };
if (subcommand === 'registration-code')
msg.type = 'reqregcode';
const response = await shard.send(msg, true); const response = await shard.send(msg, true);
if (!response) if (!response)
@ -44,7 +87,6 @@ class CreateCommand extends BaseCommand {
out += `${invite.code}\n`; out += `${invite.code}\n`;
return out; return out;
} }
} }

View File

@ -13,7 +13,7 @@ import MongoStore from 'connect-mongo';
// Own // Own
import { LogFunction, LoggerClient } from '@navy.gif/logger'; import { LogFunction, LoggerClient } from '@navy.gif/logger';
import DiscordStrategy from '@navy.gif/passport-discord'; import DiscordStrategy from '@navy.gif/passport-discord';
import { MariaDB, MongoDB } from '@navy.gif/wrappers'; import { MariaDB, MessageBroker, MongoDB } from '@navy.gif/wrappers';
// Local // Local
import { Util } from '../util/index.js'; import { Util } from '../util/index.js';
@ -37,6 +37,7 @@ class Server extends EventEmitter {
#_name: string; #_name: string;
#_userDatabase: UserDatabaseInterface; #_userDatabase: UserDatabaseInterface;
#_messageBroker: MessageBroker;
#_options: ServerOptions; #_options: ServerOptions;
#_shardId: number; #_shardId: number;
#_ready: boolean; #_ready: boolean;
@ -68,8 +69,10 @@ class Server extends EventEmitter {
MONGO_PORT, MONGO_PASS, MONGO_DB, MONGO_AUTH_DB, MONGO_PORT, MONGO_PASS, MONGO_DB, MONGO_AUTH_DB,
NODE_ENV, SECRET, CRYPTO_SECRET, CRYPTO_SALT, NODE_ENV, SECRET, CRYPTO_SECRET, CRYPTO_SALT,
MONGO_MEMORY_URI, MONGO_MEMORY_HOST, MONGO_MEMORY_USER, MONGO_MEMORY_PORT, MONGO_MEMORY_URI, MONGO_MEMORY_HOST, MONGO_MEMORY_USER, MONGO_MEMORY_PORT,
MONGO_MEMORY_PASS, MONGO_MEMORY_DB, MONGO_MEMORY_AUTH_DB } = process.env as {[key: string]: string}; MONGO_MEMORY_PASS, MONGO_MEMORY_DB, MONGO_MEMORY_AUTH_DB,
const { http: httpOpts, databases, name } = options; RABBIT_HOST, RABBIT_USER, RABBIT_PASS, RABBIT_VHOST, RABBIT_PORT
} = process.env as { [key: string]: string };
const { http: httpOpts, databases, name, rabbitConfig } = options;
// This key never leaves memory and is exclusively used on the server, the salt can stay static // This key never leaves memory and is exclusively used on the server, the salt can stay static
const encryption = Util.createEncryptionKey(CRYPTO_SECRET as string, CRYPTO_SALT as string); const encryption = Util.createEncryptionKey(CRYPTO_SECRET as string, CRYPTO_SALT as string);
@ -141,6 +144,15 @@ class Server extends EventEmitter {
}); });
this.#_userDatabase = new UserDatabase(this, this.#_mongodb); this.#_userDatabase = new UserDatabase(this, this.#_mongodb);
this.#_messageBroker = new MessageBroker(this, {
...rabbitConfig,
host: RABBIT_HOST,
user: RABBIT_USER,
pass: RABBIT_PASS,
vhost: RABBIT_VHOST,
port: parseInt(RABBIT_PORT)
});
// Provider needs to implement getKey(key) and setKey(key, value) // Provider needs to implement getKey(key) and setKey(key, value)
// Distributed memory storage, using mongo in this case, but this could be redis or whatever // Distributed memory storage, using mongo in this case, but this could be redis or whatever
this.#_memoryStoreProvider = new MongoMemory(this, { this.#_memoryStoreProvider = new MongoMemory(this, {
@ -219,6 +231,8 @@ class Server extends EventEmitter {
await this.#_userDatabase.init(); await this.#_userDatabase.init();
await this.#_messageBroker.init();
this.#logger.info('Loading endpoints'); this.#logger.info('Loading endpoints');
await this.#_registry.loadEndpoints(); await this.#_registry.loadEndpoints();
this.#logger.debug(this.#_registry.print); this.#logger.debug(this.#_registry.print);
@ -275,6 +289,7 @@ class Server extends EventEmitter {
await this.#_mongodb.close(); await this.#_mongodb.close();
await this.#_mariadb.close(); await this.#_mariadb.close();
await this.#_memoryStoreProvider.close(); await this.#_memoryStoreProvider.close();
await this.#_messageBroker.close();
this.#logger.status('DB shutdowns complete.'); this.#logger.status('DB shutdowns complete.');
this.#logger.status('Server shutdown complete.'); this.#logger.status('Server shutdown complete.');
@ -302,6 +317,17 @@ class Server extends EventEmitter {
codes.push(code); codes.push(code);
} }
process.send({ _id: msg._id, codes }); process.send({ _id: msg._id, codes });
} else if (msg.type === 'account-create') {
const name = msg.accountName as string;
const pass = msg.accountPass as string;
this.#logger.info(`Creating account ${name}`);
try {
await this.users.createUser(name, pass);
process.send({ _id: msg._id, success: true });
} catch (err) {
const error = err as Error;
process.send({ _id: msg._id, success: false, message: error.message });
}
} }
} }
@ -386,6 +412,10 @@ class Server extends EventEmitter {
return this.#_mariadb; return this.#_mariadb;
} }
get messageBroker () {
return this.#_messageBroker;
}
get users () { get users () {
return this.#_userDatabase; return this.#_userDatabase;
} }

View File

@ -0,0 +1,107 @@
import { MongoDB, ObjectId } from "@navy.gif/wrappers";
import Server from "../Server.js";
import Flag from "../structures/Flag.js";
import { FlagConsumer, FlagData, FlagEnv } from "../../../@types/Flags.js";
import { Collection } from "@discordjs/collection";
import { LoggerClient } from "@navy.gif/logger";
type FlagQuery = {
id?: string,
name?: string,
env?: FlagEnv,
consumer?: FlagConsumer
}
class FlagManager {
#server: Server;
#mongo: MongoDB;
#collectionName = 'flags';
#flags: Collection<string, Flag>;
#broker;
#logger: LoggerClient;
constructor (server: Server) {
this.#server = server;
this.#mongo = server.mongodb;
this.#logger = server.createLogger(this);
this.#flags = new Collection();
this.#broker = server.messageBroker;
}
async init () {
const data = await this.#mongo.find<FlagData>(this.#collectionName, {});
for (const flagData of data) {
flagData._id = flagData._id.toString();
const flag = new Flag(this, flagData);
this.#flags.set(flag.id, flag);
}
this.#broker.subscribe('flagUpdates', this.#flagUpdate.bind(this));
}
async #flagUpdate (incoming: { origin: number, flag: FlagData }) {
const { flag: data, origin } = incoming;
if (origin === this.#server.shardId)
return;
this.#logger.info(`Incoming flag update for ${data.name}`);
const flag = this.#flags.get(data._id as string);
if (!flag) {
if (!data._id)
throw new Error(`Missing flag id? ${data.name}`);
this.#flags.set(data._id, new Flag(this, data));
return;
}
flag.value = data.value;
}
getFlags ({ id, name, env, consumer }: FlagQuery): Flag[] {
if (id) {
const flag = this.#flags.get(id);
if (!flag)
return [];
return [ flag ];
}
let filtered = this.#flags;
if (name)
filtered = filtered.filter(flag => flag.name === name);
if (env)
filtered = filtered.filter(flag => flag.env === env);
if (consumer)
filtered = filtered.filter(flag => flag.consumer === consumer);
return [ ...filtered.values() ];
}
async createFlag (data: FlagData) {
const existing = await this.#mongo.findOne(this.#collectionName, { name: data.name, env: data.env, consumer: data.consumer });
if (existing)
throw new Error(`A flag with the given parameters already exists`);
data._id = (new ObjectId()).toString();
const flag = new Flag(this, data);
await this.#mongo.insertOne(this.#collectionName, flag.json);
this.#flags.set(flag.id, flag);
this.#broker.publish('flagUpdates', { origin: this.#server.shardId, flag: flag.json });
return flag;
}
async updateFlag (flag: Flag): Promise<void> {
const json = flag.json as { _id?: string };
delete json._id;
await this.#mongo.updateOne(this.#collectionName, { _id: flag.id }, json, true);
this.#broker.publish('flagUpdates', { origin: this.#server.shardId, flag: flag.json });
}
}
export default FlagManager;

View File

@ -61,7 +61,7 @@ class UserDatabase implements UserDatabaseInterface {
async init () { async init () {
for (const coll of [ this.#_userCollection, this.#_appCollection, this.#_roleCollection ]) for (const coll of [ this.#_userCollection, this.#_appCollection, this.#_roleCollection ])
await this.#db.ensureIndex(coll, [ 'name' ]); await this.#db.ensureIndex(coll, [ 'name' ], { unique: true });
} }
async fetchUsers (page: number, amount = 10, query = {}): Promise<User[]> { async fetchUsers (page: number, amount = 10, query = {}): Promise<User[]> {

View File

@ -0,0 +1,162 @@
import { FlagConsumer, FlagData, FlagEnv, FlagType } from "../../../@types/Flags.js";
import FlagManager from "../components/FlagManager.js";
// const BITS = {
// Client: (1 << 0),
// Server: (1 << 1),
// Test: (1 << 2),
// Prod: (1 << 3)
// };
const CONSUMERS = [ 'client', 'server', 'api' ];
const ENVS = [ 'test', 'prod' ];
class Flag {
#manager: FlagManager;
#_id: string;
#_name: string;
#_env: FlagEnv;
#_consumer: FlagConsumer;
#_value: FlagType;
#_type: string;
constructor (manager: FlagManager, data: FlagData) {
this.#manager = manager;
if (!data._id)
throw new Error('Missing Id');
if (!data.name)
throw new Error('Missing name');
if (!data.consumer)
throw new Error('Missing consumer');
if (!CONSUMERS.includes(data.consumer))
throw new Error('Bad consumer');
if (!data.env)
throw new Error('Missing env');
if (!ENVS.includes(data.env))
throw new Error('Bad env');
if (!('value' in data))
throw new Error('Missing value');
this.#_id = data._id;
this.#_env = data.env;
this.#_consumer = data.consumer;
this.#_name = data.name;
this.#_value = data.value;
this.#_type = Flag.resolveType(data.value);
}
save (): Promise<void> {
return this.#manager.updateFlag(this);
}
get name () {
return this.#_name;
}
get id () {
return this.#_id;
}
get type () {
return this.#_type;
}
get value () {
return this.#_value;
}
set value (val) {
this.#_value = val;
}
get env () {
return this.#_env;
}
get consumer () {
return this.#_consumer;
}
get json () {
return {
_id: this.id,
name: this.name,
value: this.value,
env: this.env,
consumer: this.consumer
};
}
// get client () {
// return (this.#env & BITS.Client) === BITS.Client;
// }
// set client (val: boolean) {
// if (val)
// this.#env |= BITS.Client;
// else
// this.#env &= ~BITS.Client;
// }
// get server () {
// return (this.#env & BITS.Server) === BITS.Server;
// }
// set server (val: boolean) {
// if (val)
// this.#env |= BITS.Server;
// else
// this.#env &= ~BITS.Server;
// }
// get test () {
// return (this.#env & BITS.Test) === BITS.Test;
// }
// set test (val: boolean) {
// if (val)
// this.#env |= BITS.Test;
// else
// this.#env &= ~BITS.Test;
// }
// get prod () {
// return (this.#env & BITS.Prod) === BITS.Prod;
// }
// set prod (val: boolean) {
// if (val)
// this.#env |= BITS.Prod;
// else
// this.#env &= ~BITS.Prod;
// }
static resolveType (value: unknown) {
if (!value)
throw new Error('Missing value');
let type = '';
const nativeType = typeof value;
if (nativeType === 'object') {
type = value.constructor.name;
if (value instanceof Array) {
const compositeType = typeof value[0];
type += `<${compositeType}>`;
}
} else {
type = nativeType;
}
return type;
}
}
export default Flag;

View File

@ -1773,15 +1773,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@navy.gif/wrappers@npm:^1.3.13": "@navy.gif/wrappers@npm:^1.3.14":
version: 1.3.13 version: 1.3.14
resolution: "@navy.gif/wrappers@npm:1.3.13" resolution: "@navy.gif/wrappers@npm:1.3.14"
dependencies: dependencies:
amqp-connection-manager: ^4.1.12 amqp-connection-manager: ^4.1.12
amqplib: ^0.10.3 amqplib: ^0.10.3
mongodb: ^5.2.0 mongodb: ^5.2.0
mysql: ^2.18.1 mysql: ^2.18.1
checksum: e3328375a8ca4ce6995dcc777b91dece35d92edad1046e6c728226de99a5e524b992fb6f2cb78dd7a266488f5c87294478088c5c0726304ca6b0f3ce56f8668c checksum: 0b9d102b34bdb7f423955f0202bf908aca5b6355621feb00ca2466b60e20f87c5e74953e05a3c5932ff2ee6f508940ec694862b7b93bc2c3ecd8937f25878209
languageName: node languageName: node
linkType: hard linkType: hard
@ -7038,7 +7038,7 @@ __metadata:
"@navy.gif/commandparser": ^1.4.5 "@navy.gif/commandparser": ^1.4.5
"@navy.gif/logger": ^2.3.3 "@navy.gif/logger": ^2.3.3
"@navy.gif/passport-discord": ^0.2.2-b "@navy.gif/passport-discord": ^0.2.2-b
"@navy.gif/wrappers": ^1.3.13 "@navy.gif/wrappers": ^1.3.14
"@types/cors": ^2.8.13 "@types/cors": ^2.8.13
"@types/express-fileupload": ^1.4.1 "@types/express-fileupload": ^1.4.1
"@types/express-session": ^1.17.7 "@types/express-session": ^1.17.7