Merge pull request #245 from XWasHere/dev/0.15.0

pull/248/head
Josh Moore 8 months ago committed by GitHub
commit 347b80e5c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
import { UserConfiguration, UserConfigTypeChecker } from 'ass';
import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass';
import fs from 'fs-extra';
import { path } from '@tycrek/joint';
@ -56,7 +56,10 @@ const Checkers: UserConfigTypeChecker = {
host: basicStringChecker,
user: basicStringChecker,
password: basicStringChecker,
database: basicStringChecker
database: basicStringChecker,
},
postgres: {
port: (val) => numChecker(val) && val >= 1 && val <= 65535
}
},
@ -102,12 +105,22 @@ export class UserConfig {
if (!Checkers.s3.credentials.secretKey(config.s3.credentials.secretKey)) throw new Error('Invalid S3 Secret key');
}
// * Optional SQL config(s) (Currently only checks MySQL)
if (config.sql?.mySql != null) {
if (!Checkers.sql.mySql.host(config.sql.mySql.host)) throw new Error('Invalid MySql Host');
if (!Checkers.sql.mySql.user(config.sql.mySql.user)) throw new Error('Invalid MySql User');
if (!Checkers.sql.mySql.password(config.sql.mySql.password)) throw new Error('Invalid MySql Password');
if (!Checkers.sql.mySql.database(config.sql.mySql.database)) throw new Error('Invalid MySql Database');
// * Optional database config(s)
if (config.database != null) {
// these both have the same schema so we can just check both
if (config.database.kind == 'mysql' || config.database.kind == 'postgres') {
if (config.database.options != undefined) {
if (!Checkers.sql.mySql.host(config.database.options.host)) throw new Error('Invalid database host');
if (!Checkers.sql.mySql.user(config.database.options.user)) throw new Error('Invalid databse user');
if (!Checkers.sql.mySql.password(config.database.options.password)) throw new Error('Invalid database password');
if (!Checkers.sql.mySql.database(config.database.options.database)) throw new Error('Invalid database');
if (config.database.kind == 'postgres') {
if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration).port)) {
throw new Error("Invalid database port");
}
}
} else throw new Error('Database options missing');
}
}
// * optional rate limit config

@ -9,9 +9,12 @@ import { path, isProd } from '@tycrek/joint';
import { epcss } from '@tycrek/express-postcss';
import { log } from './log';
import { ensureFiles, get } from './data';
import { get } from './data';
import { UserConfig } from './UserConfig';
import { MySql } from './sql/mysql';
import { DBManager } from './sql/database';
import { JSONDatabase } from './sql/json';
import { MySQLDatabase } from './sql/mysql';
import { PostgreSQLDatabase } from './sql/postgres';
import { buildFrontendRouter } from './routers/_frontend';
/**
@ -76,8 +79,9 @@ async function main() {
App.pkgVersion = pkg.version;
// Ensure data files exist
await ensureFiles();
// Ensure data directory exists
log.debug('Checking data dir')
await fs.ensureDir(path.join('.ass-data'));
// Set default server configuration
const serverConfig: ServerConfiguration = {
@ -112,9 +116,24 @@ async function main() {
.catch((err) => (err.code && err.code === 'ENOENT' ? {} : console.error(err), resolve(void 0))));
// If user config is ready, try to configure SQL
if (UserConfig.ready && UserConfig.config.sql?.mySql != null)
try { await MySql.configure(); }
catch (err) { throw new Error(`Failed to configure SQL`); }
if (UserConfig.ready && UserConfig.config.database != null) {
try {
switch (UserConfig.config.database?.kind) {
case 'json':
await DBManager.use(new JSONDatabase());
break;
case 'mysql':
await DBManager.use(new MySQLDatabase());
break;
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
}
} catch (err) { throw new Error(`Failed to configure SQL`); }
} else { // default to json database
log.debug('DB not set! Defaulting to JSON');
await DBManager.use(new JSONDatabase());
}
// Set up Express
const app = express();

@ -1,12 +1,8 @@
import { AssFile, AssUser, NID, FilesSchema, UsersSchema } from 'ass';
import fs from 'fs-extra';
import { path } from '@tycrek/joint';
import { AssFile, AssUser, NID } from 'ass';
import { log } from './log';
import { nanoid } from './generators';
import { UserConfig } from './UserConfig';
import { MySql } from './sql/mysql';
import { DBManager } from './sql/database';
/**
* Switcher type for exported functions
@ -14,156 +10,25 @@ import { MySql } from './sql/mysql';
type DataSector = 'files' | 'users';
/**
* Absolute filepaths for JSON data files
* database kind -> name mapping
*/
const PATHS = {
files: path.join('.ass-data/files.json'),
users: path.join('.ass-data/users.json')
};
const bothWriter = async (files: FilesSchema, users: UsersSchema) => {
await fs.writeJson(PATHS.files, files, { spaces: '\t' });
await fs.writeJson(PATHS.users, users, { spaces: '\t' });
const DBNAMES = {
'mysql': 'MySQL',
'postgres': 'PostgreSQL',
'json': 'JSON'
};
/**
* Creates a JSON file with a given empty data template
*/
const createEmptyJson = (filepath: string, emptyData: any): Promise<void> => new Promise(async (resolve, reject) => {
try {
if (!(await fs.pathExists(filepath))) {
await fs.ensureFile(filepath);
await fs.writeJson(filepath, emptyData, { spaces: '\t' });
}
resolve(void 0);
} catch (err) {
reject(err);
}
});
/**
* Ensures the data files exist and creates them if required
*/
export const ensureFiles = (): Promise<void> => new Promise(async (resolve, reject) => {
log.debug('Checking data files');
try {
// Create data directory
await fs.ensureDir(path.join('.ass-data'));
// * Default files.json
await createEmptyJson(PATHS.files, {
files: {},
useSql: false,
meta: {}
} as FilesSchema);
// * Default users.json
await createEmptyJson(PATHS.users, {
tokens: [],
users: {},
cliKey: nanoid(32),
useSql: false,
meta: {}
} as UsersSchema);
log.debug('Data files exist');
resolve();
} catch (err) {
log.error('Failed to verify existence of data files');
reject(err);
}
});
export const setDataModeToSql = (): Promise<void> => new Promise(async (resolve, reject) => {
log.debug('Setting data mode to SQL');
// Main config check
if (!UserConfig.ready || !UserConfig.config.sql?.mySql) return reject(new Error('MySQL not configured'));
const mySqlConf = UserConfig.config.sql.mySql;
// Read data files
const [files, users]: [FilesSchema, UsersSchema] = await Promise.all([fs.readJson(PATHS.files), fs.readJson(PATHS.users)]);
// Check the MySQL configuration
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(mySqlConf.host) ? 'Missing MySQL Host'
: !checker(mySqlConf.user) ? 'Missing MySQL User'
: !checker(mySqlConf.password) ? 'Missing MySQL Password'
: !checker(mySqlConf.database) ? 'Missing MySQL Database'
// ! Blame VS Code for this weird indentation
: undefined;
// Set the vars
files.useSql = issue == null;
users.useSql = issue == null;
// Write data & return
await bothWriter(files, users);
(issue) ? reject(new Error(issue)) : resolve(void 0);
});
export const put = (sector: DataSector, key: NID, data: AssFile | AssUser): Promise<void> => new Promise(async (resolve, reject) => {
try {
const useSql = MySql.ready;
if (sector === 'files') {
// * 1: Save as files (image, video, etc)
data = data as AssFile;
if (!useSql) {
// ? Local JSON
const filesJson = await fs.readJson(PATHS.files) as FilesSchema;
// Check if key already exists
if (filesJson.files[key] != null) return reject(new Error(`File key ${key} already exists`));
// Otherwise add the data
filesJson.files[key] = data;
// Also save the key to the users file
const usersJson = await fs.readJson(PATHS.users) as UsersSchema;
// todo: uncomment this once users are implemented
// usersJson.users[data.uploader].files.push(key);
// Save the files
await bothWriter(filesJson, usersJson);
} else {
// ? SQL
if (!(await MySql.get('assfiles', key))) await MySql.put('assfiles', key, data);
else return reject(new Error(`File key ${key} already exists`));
// todo: modify users SQL files property
}
await DBManager.put('assfiles', key, data as AssFile);
} else {
// * 2: Save as users
data = data as AssUser;
if (!useSql) {
// ? Local JSON
const usersJson = await fs.readJson(PATHS.users) as UsersSchema;
// Check if key already exists
if (usersJson.users[key] != null) return reject(new Error(`User key ${key} already exists`));
// Otherwise add the data
usersJson.users[key] = data;
await fs.writeJson(PATHS.users, usersJson, { spaces: '\t' });
} else {
// ? SQL
if (!(await MySql.get('assusers', key))) await MySql.put('assusers', key, data);
else return reject(new Error(`User key ${key} already exists`));
}
await DBManager.put('assusers', key, data as AssUser);
}
log.info(`PUT ${sector} data`, `using ${useSql ? 'SQL' : 'local JSON'}`, key);
log.info(`PUT ${sector} data`, `using ${DBNAMES[UserConfig.config.database?.kind ?? 'json']}`, key);
resolve(void 0);
} catch (err) {
reject(err);
@ -172,22 +37,18 @@ export const put = (sector: DataSector, key: NID, data: AssFile | AssUser): Prom
export const get = (sector: DataSector, key: NID): Promise<AssFile | AssUser | false> => new Promise(async (resolve, reject) => {
try {
const data: AssFile | AssUser | undefined = (MySql.ready)
? (await MySql.get(sector === 'files' ? 'assfiles' : 'assusers', key) as AssFile | AssUser | undefined)
: (await fs.readJson(PATHS[sector]))[sector][key];
const data: AssFile | AssUser | undefined = await DBManager.get(sector === 'files' ? 'assfiles' : 'assusers', key) as AssFile | AssUser | undefined
(!data) ? resolve(false) : resolve(data);
} catch (err) {
reject(err);
}
});
export const getAll = (sector: DataSector): Promise<{ [key: string]: AssFile | AssUser } | false> => new Promise(async (resolve, reject) => {
export const getAll = (sector: DataSector): Promise<{ [key: string]: AssFile | AssUser }> => new Promise(async (resolve, reject) => {
try {
const data: { [key: string]: AssFile | AssUser } | undefined = (MySql.ready)
// todo: fix MySQL
? (await MySql.getAll(sector === 'files' ? 'assfiles' : 'assusers') as /* AssFile[] | AssUser[] | */ undefined)
: (await fs.readJson(PATHS[sector]))[sector];
(!data) ? resolve(false) : resolve(data);
// todo: fix MySQL
const data: { [key: string]: AssFile | AssUser } = await DBManager.getAll(sector === 'files' ? 'assfiles' : 'assusers') as /* AssFile[] | AssUser[] | */ {}
resolve(data);
} catch (err) {
reject(err);
}

@ -7,8 +7,11 @@ import * as data from '../data';
import { log } from '../log';
import { nanoid } from '../generators';
import { UserConfig } from '../UserConfig';
import { MySql } from '../sql/mysql';
import { rateLimiterMiddleware, setRateLimiter } from '../ratelimit';
import { DBManager } from '../sql/database';
import { JSONDatabase } from '../sql/json';
import { MySQLDatabase } from '../sql/mysql';
import { PostgreSQLDatabase } from '../sql/postgres';
const router = Router({ caseSensitive: true });
@ -26,14 +29,25 @@ router.post('/setup', BodyParserJson(), async (req, res) => {
// Save config
await UserConfig.saveConfigFile();
// Set data storage (not files) to SQL if required
if (UserConfig.config.sql?.mySql != null)
await Promise.all([MySql.configure(), data.setDataModeToSql()]);
// set up new databases
if (UserConfig.config.database) {
switch (UserConfig.config.database.kind) {
case 'json':
await DBManager.use(new JSONDatabase());
break;
case 'mysql':
await DBManager.use(new MySQLDatabase());
break;
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
}
}
// set rate limits
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);;
log.success('Setup', 'completed');

@ -0,0 +1,105 @@
import { AssFile, AssUser, NID, UploadToken } from "ass";
export type DatabaseValue = AssFile | AssUser | UploadToken;
export type DatabaseTable = 'assfiles' | 'assusers' | 'asstokens';
/**
* interface for database classes
*/
export interface Database {
/**
* preform database initialization tasks
*/
open(): Promise<void>;
/**
* preform database suspension tasks
*/
close(): Promise<void>;
/**
* set up database
*/
configure(): Promise<void>;
/**
* put a value in the database
*/
put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void>;
/**
* get a value from the database
*/
get(table: DatabaseTable, key: NID): Promise<DatabaseValue | undefined>;
/**
* get all values from the database
*/
getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }>;
}
export class DBManager {
private static _db: Database;
private static _dbReady: boolean = false;
public static get ready() {
return this._dbReady;
}
static {
process.on('exit', () => {
if (DBManager._db) DBManager._db.close();
});
}
/**
* activate a database
*/
public static use(db: Database): Promise<void> {
return new Promise(async (resolve, reject) => {
if (this._db != undefined) {
await this._db.close();
this._dbReady = false;
}
this._db = db;
await this._db.open();
await this._db.configure();
this._dbReady = true;
resolve();
});
}
public static configure(): Promise<void> {
if (this._db && this._dbReady) {
return this._db.configure();
} else throw new Error("No database active");
}
/**
* put a value in the database
*/
public static put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void> {
if (this._db && this._dbReady) {
return this._db.put(table, key, data);
} else throw new Error("No database active");
}
/**
* get a value from the database
*/
public static get(table: DatabaseTable, key: NID): Promise<DatabaseValue | undefined> {
if (this._db && this._dbReady) {
return this._db.get(table, key);
} else throw new Error("No database active");
}
/**
* get all values from the database
*/
public static getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }> {
if (this._db && this._dbReady) {
return this._db.getAll(table);
} else throw new Error("No database active");
}
}

@ -0,0 +1,152 @@
import { AssFile, AssUser, FilesSchema, UsersSchema } from 'ass';
import path, { resolve } from 'path';
import fs from 'fs-extra';
import { Database, DatabaseTable, DatabaseValue } from './database';
import { log } from '../log';
import { nanoid } from '../generators';
/**
* Absolute filepaths for JSON data files
*/
const PATHS = {
files: path.join('.ass-data/files.json'),
users: path.join('.ass-data/users.json')
};
/**
* map from tables to paths
*/
const PATHMAP = {
assfiles: PATHS.files,
assusers: PATHS.users
} as { [index: string]: string };
/**
* map from tables to sectors
*/
const SECTORMAP = {
assfiles: 'files',
assusers: 'users'
} as { [index: string]: string };
const bothWriter = async (files: FilesSchema, users: UsersSchema) => {
await fs.writeJson(PATHS.files, files, { spaces: '\t' });
await fs.writeJson(PATHS.users, users, { spaces: '\t' });
};
/**
* Creates a JSON file with a given empty data template
*/
const createEmptyJson = (filepath: string, emptyData: any): Promise<void> => new Promise(async (resolve, reject) => {
try {
if (!(await fs.pathExists(filepath))) {
await fs.ensureFile(filepath);
await fs.writeJson(filepath, emptyData, { spaces: '\t' });
}
resolve(void 0);
} catch (err) {
reject(err);
}
});
/**
* Ensures the data files exist and creates them if required
*/
export const ensureFiles = (): Promise<void> => new Promise(async (resolve, reject) => {
log.debug('Checking data files');
try {
// * Default files.json
await createEmptyJson(PATHS.files, {
files: {},
useSql: false,
meta: {}
} as FilesSchema);
// * Default users.json
await createEmptyJson(PATHS.users, {
tokens: [],
users: {},
cliKey: nanoid(32),
useSql: false,
meta: {}
} as UsersSchema);
log.debug('Data files exist');
resolve();
} catch (err) {
log.error('Failed to verify existence of data files');
reject(err);
}
});
/**
* JSON database. i know json isnt sql, shut up.
*/
export class JSONDatabase implements Database {
public open(): Promise<void> { return Promise.resolve() }
public close(): Promise<void> { return Promise.resolve() }
public configure(): Promise<void> {
return new Promise((resolve, reject) => {
ensureFiles();
resolve();
});
}
public put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
if (table == 'assfiles') {
// ? Local JSON
const filesJson = await fs.readJson(PATHS.files) as FilesSchema;
// Check if key already exists
if (filesJson.files[key] != null) return reject(new Error(`File key ${key} already exists`));
// Otherwise add the data
filesJson.files[key] = data as AssFile;
// Also save the key to the users file
const usersJson = await fs.readJson(PATHS.users) as UsersSchema;
// todo: uncomment this once users are implemented
// usersJson.users[data.uploader].files.push(key);
// Save the files
await bothWriter(filesJson, usersJson);
resolve()
} else if (table == 'assusers') {
// ? Local JSON
const usersJson = await fs.readJson(PATHS.users) as UsersSchema;
// Check if key already exists
if (usersJson.users[key] != null) return reject(new Error(`User key ${key} already exists`));
// Otherwise add the data
usersJson.users[key] = data as AssUser;
await fs.writeJson(PATHS.users, usersJson, { spaces: '\t' });
resolve();
}
})
}
public get(table: DatabaseTable, key: string): Promise<DatabaseValue | undefined> {
return new Promise(async (resolve, reject) => {
const data = (await fs.readJson(PATHMAP[table]))[SECTORMAP[table]][key];
(!data) ? resolve(undefined) : resolve(data);
});
}
public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }> {
return new Promise(async (resolve, reject) => {
const data = (await fs.readJson(PATHMAP[table]))[SECTORMAP[table]];
(!data) ? resolve({}) : resolve(data);
});
}
}

@ -4,21 +4,20 @@ import mysql, { Pool } from 'mysql2/promise';
import { log } from '../log';
import { UserConfig } from '../UserConfig';
import { Database, DatabaseTable, DatabaseValue } from './database';
type TableNamesType = 'assfiles' | 'assusers' | 'asstokens';
export class MySQLDatabase implements Database {
private _pool: Pool;
export class MySql {
private static _pool: Pool;
private static _ready: boolean = false;
public static get ready() { return MySql._ready; }
private _ready: boolean = false;
public get ready() { return this._ready; }
/**
* Quick function for creating a simple JSON table
*/
private static _tableManager(mode: 'create' | 'drop', name: string, schema = '( NanoID varchar(255), Data JSON )'): Promise<void> {
private _tableManager(mode: 'create' | 'drop', name: string, schema = '( NanoID varchar(255), Data JSON )'): Promise<void> {
return new Promise((resolve, reject) =>
MySql._pool.query(
this._pool.query(
mode === 'create'
? `CREATE TABLE ${name} ${schema};`
: `DROP TABLE ${name};`)
@ -26,31 +25,58 @@ export class MySql {
.catch((err) => reject(err)));
}
/**
* validate the mysql config
*/
private _validateConfig(): string | undefined {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MySQL configuration missing';
if (UserConfig.config.database.kind != "mysql") return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MySQL configuration missing';
let mySqlConf = UserConfig.config.database.options;
// Check the MySQL configuration
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(mySqlConf.host) ? 'Missing MySQL Host'
: !checker(mySqlConf.user) ? 'Missing MySQL User'
: !checker(mySqlConf.password) ? 'Missing MySQL Password'
: !checker(mySqlConf.database) ? 'Missing MySQL Database'
// ! Blame VS Code for this weird indentation
: undefined;
return issue;
}
public open() { return Promise.resolve(); }
public close() { return Promise.resolve(); }
/**
* Build the MySQL client and create the tables
*/
public static configure(): Promise<void> {
public configure(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// Config check
if (!UserConfig.ready) throw new Error('User configuration not ready');
if (!UserConfig.config.sql?.mySql) throw new Error('MySQL configuration missing');
let configError = this._validateConfig();
if (configError) throw new Error(configError);
// Create the pool
MySql._pool = mysql.createPool(UserConfig.config.sql.mySql);
this._pool = mysql.createPool(UserConfig.config.database!.options!);
// Check if the pool is usable
const [rowz, _fields] = await MySql._pool.query(`SHOW FULL TABLES WHERE Table_Type LIKE 'BASE TABLE';`);
const [rowz, _fields] = await this._pool.query(`SHOW FULL TABLES WHERE Table_Type LIKE 'BASE TABLE';`);
const rows_tableData = rowz as unknown as { [key: string]: string }[];
// Create tables if needed
if (rows_tableData.length === 0) {
log.warn('MySQL', 'Tables do not exist, creating');
await Promise.all([
MySql._tableManager('create', 'assfiles'),
MySql._tableManager('create', 'assusers'),
MySql._tableManager('create', 'asstokens')
this._tableManager('create', 'assfiles'),
this._tableManager('create', 'assusers'),
this._tableManager('create', 'asstokens')
]);
log.success('MySQL', 'Tables created').callback(resolve);
} else {
@ -60,8 +86,8 @@ export class MySql {
// Check which tables ACTUALLY do exist
for (let row of rows_tableData) {
const table = row[`Tables_in_${UserConfig.config.sql!.mySql!.database}`
] as TableNamesType;
const table = row[`Tables_in_${UserConfig.config.database!.options!.database}`
] as DatabaseTable;
if (table === 'assfiles') tablesExist.files = true;
if (table === 'assusers') tablesExist.users = true;
if (table === 'asstokens') tablesExist.tokens = true;
@ -69,9 +95,9 @@ export class MySql {
}
// Mini-function for creating a one-off table
const createOneTable = async (name: TableNamesType) => {
const createOneTable = async (name: DatabaseTable) => {
log.warn('MySQL', `Table '${name}' missing, creating`);
await MySql._tableManager('create', name);
await this._tableManager('create', name);
log.success('MySQL', `Table '${name}' created`);
}
@ -88,7 +114,7 @@ export class MySql {
// Hopefully we are ready
if (tablesExist.files && tablesExist.users)
log.info('MySQL', 'Tables exist, ready').callback(() => {
MySql._ready = true;
this._ready = true;
resolve(void 0);
});
else throw new Error('Table(s) missing!');
@ -101,26 +127,28 @@ export class MySql {
});
}
public static put(table: TableNamesType, key: NID, data: UploadToken | AssFile | AssUser): Promise<void> {
public put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
if (!MySql._ready) return reject(new Error('MySQL not ready'));
if (!this._ready) return reject(new Error('MySQL not ready'));
if (await this.get(table, key)) reject(new Error(`${table == 'assfiles' ? 'File' : table == 'assusers' ? 'User' : 'Token'} key ${key} already exists`));
const query = `
INSERT INTO ${table} ( NanoID, Data )
VALUES ('${key}', '${JSON.stringify(data)}');
`;
return MySql._pool.query(query)
return this._pool.query(query)
.then(() => resolve(void 0))
.catch((err) => reject(err));
});
}
public static get(table: TableNamesType, key: NID): Promise<UploadToken | AssFile | AssUser | undefined> {
public get(table: DatabaseTable, key: NID): Promise<DatabaseValue | undefined> {
return new Promise(async (resolve, reject) => {
try {
// Run query
const [rowz, _fields] = await MySql._pool.query(`SELECT Data FROM ${table} WHERE NanoID = '${key}';`);
const [rowz, _fields] = await this._pool.query(`SELECT Data FROM ${table} WHERE NanoID = '${key}';`);
// Disgustingly interpret the query results
const rows_tableData = (rowz as unknown as { [key: string]: string }[])[0] as unknown as ({ Data: UploadToken | AssFile | AssUser | undefined });
@ -133,11 +161,11 @@ VALUES ('${key}', '${JSON.stringify(data)}');
}
// todo: unknown if this works
public static getAll(table: TableNamesType): Promise<UploadToken | AssFile | AssUser | undefined> {
public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }> {
return new Promise(async (resolve, reject) => {
try {
// Run query // ! this may not work as expected
const [rowz, _fields] = await MySql._pool.query(`SELECT Data FROM ${table}`);
const [rowz, _fields] = await this._pool.query(`SELECT Data FROM ${table}`);
// Interpret results this is pain
const rows = (rowz as unknown as { [key: string]: string }[]);
@ -145,7 +173,7 @@ VALUES ('${key}', '${JSON.stringify(data)}');
// console.log(rows);
// aaaaaaaaaaaa
resolve(undefined);
resolve({});
} catch (err) {
reject(err);
}

@ -0,0 +1,201 @@
import { PostgresConfiguration } from 'ass';
import { Client } from 'pg';
import { log } from '../log';
import { Database, DatabaseTable, DatabaseValue } from './database';
import { UserConfig } from '../UserConfig';
/**
* database adapter for postgresql
*/
export class PostgreSQLDatabase implements Database {
private _client: Client;
/**
* validate config
*/
private _validateConfig(): string | undefined {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'PostgreSQL configuration missing';
if (UserConfig.config.database.kind != "postgres") return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'PostgreSQL configuration missing';
let config = UserConfig.config.database.options;
// check the postgres config
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(config.host) ? 'Missing PostgreSQL Host'
: !checker(config.user) ? 'Missing PostgreSQL User'
: !checker(config.password) ? 'Missing PostgreSQL Password'
: !checker(config.database) ? 'Missing PostgreSQL Database'
// ! Blame VS Code for this weird indentation
: undefined;
return issue;
}
public open(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// config check
let configError = this._validateConfig();
if (configError) throw new Error(configError);
// grab the config
let config = UserConfig.config.database!.options! as PostgresConfiguration;
// set up the client
this._client = new Client({
host: config.host,
port: config.port,
user: config.user,
password: config.password,
database: config.database,
});
// connect to the database
log.info('PostgreSQL', `connecting to ${config.host}:${config.port}`);
await this._client.connect();
log.success('PostgreSQL', 'ok');
resolve();
} catch (err) {
log.error('PostgreSQL', 'failed to connect');
console.error(err);
reject(err);
}
});
}
public close(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// gracefully disconnect
await this._client.end();
resolve();
} catch (err) {
log.error('PostgreSQL', 'failed to disconnect');
console.error(err);
reject(err);
}
});
}
public configure(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
await this._client.query(
`CREATE TABLE IF NOT EXISTS asstables (
name TEXT PRIMARY KEY,
version INT NOT NULL
);`);
log.info('PostgreSQL', 'checking database');
// update tables
let seenRows = new Set<string>();
let versions = await this._client.query('SELECT * FROM asstables;');
for (let row of versions.rows) {
seenRows.add(row.name);
}
const assTableSchema = '(id TEXT PRIMARY KEY, data JSON NOT NULL)'
// add missing tables
if (!seenRows.has('assfiles')) {
log.warn('PostgreSQL', 'assfiles missing, repairing...')
await this._client.query(
`CREATE TABLE assfiles ${assTableSchema};` +
`INSERT INTO asstables (name, version) VALUES ('assfiles', 1);`
);
log.success('PostgreSQL', 'ok');
}
if (!seenRows.has('assusers')) {
log.warn('PostgreSQL', 'asstokens missing, repairing...')
await this._client.query(
`CREATE TABLE assusers ${assTableSchema};` +
`INSERT INTO asstables (name, version) VALUES ('assusers', 1);`
);
log.success('PostgreSQL', 'ok');
}
if (!seenRows.has('asstokens')) {
log.warn('PostgreSQL', 'asstokens missing, repairing...')
await this._client.query(
`CREATE TABLE asstokens ${assTableSchema};` +
`INSERT INTO asstables (name, version) VALUES ('asstokens', 1);`
);
log.success('PostgreSQL', 'ok');
}
log.success('PostgreSQL', 'database is ok').callback(() => {
resolve();
});
} catch (err) {
log.error('PostgreSQL', 'failed to set up');
console.error(err);
reject(err);
}
});
}
public put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const queries = {
assfiles: 'INSERT INTO assfiles (id, data) VALUES ($1, $2);',
assusers: 'INSERT INTO assusers (id, data) VALUES ($1, $2);',
asstokens: 'INSERT INTO asstokens (id, data) VALUES ($1, $2);'
};
let result = await this._client.query(queries[table], [key, data]);
resolve();
} catch (err) {
reject(err);
}
});
}
public get(table: DatabaseTable, key: string): Promise<DatabaseValue | undefined> {
return new Promise(async (resolve, reject) => {
try {
const queries = {
assfiles: 'SELECT data FROM assfiles WHERE id = $1::text;',
assusers: 'SELECT data FROM assusers WHERE id = $1::text;',
asstokens: 'SELECT data FROM asstokens WHERE id = $1::text;'
};
let result = await this._client.query(queries[table], [key]);
resolve(result.rowCount ? result.rows[0].data : void 0);
} catch (err) {
reject(err);
}
});
}
public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue; }> {
return new Promise(async (resolve, reject) => {
try {
const queries = {
assfiles: 'SELECT json_object_agg(id, data) AS stuff FROM assfiles;',
assusers: 'SELECT json_object_agg(id, data) AS stuff FROM assusers;',
asstokens: 'SELECT json_object_agg(id, data) AS stuff FROM asstokens;'
};
let result = await this._client.query(queries[table]);
resolve(result.rowCount ? result.rows[0].stuff : void 0);
} catch (err) {
reject(err);
}
});
}
}

32
common/types.d.ts vendored

@ -24,7 +24,7 @@ declare module 'ass' {
maximumFileSize: number;
s3?: S3Configuration;
sql?: SqlConfiguration;
database?: DatabaseConfiguration;
rateLimit?: RateLimitConfiguration;
}
@ -51,13 +51,24 @@ declare module 'ass' {
}
}
interface SqlConfiguration {
mySql?: {
host: string;
user: string;
password: string;
database: string;
}
interface DatabaseConfiguration {
kind: 'mysql' | 'postgres' | 'json';
options?: MySQLConfiguration | PostgresConfiguration;
}
interface MySQLConfiguration {
host: string;
user: string;
password: string;
database: string;
}
interface PostgresConfiguration {
host: string;
port: number;
user: string;
password: string;
database: string;
}
/**
@ -119,6 +130,9 @@ declare module 'ass' {
password: (val: any) => boolean;
database: (val: any) => boolean;
}
postgres: {
port: (val: any) => boolean;
}
}
rateLimit: {
endpoint: (val: any) => boolean;
@ -219,7 +233,6 @@ declare module 'ass' {
files: {
[key: NID]: AssFile;
}
useSql: boolean;
meta: { [key: string]: any };
}
@ -232,7 +245,6 @@ declare module 'ass' {
[key: NID]: AssUser;
};
cliKey: string;
useSql: boolean;
meta: { [key: string]: any };
}
}

@ -1,4 +1,4 @@
import { SlInput, SlButton } from '@shoelace-style/shoelace';
import { SlInput, SlButton, SlTab } from '@shoelace-style/shoelace';
import { IdType, UserConfiguration } from 'ass';
const genericErrorAlert = () => alert('An error occured, please check the console for details');
@ -41,11 +41,21 @@ document.addEventListener('DOMContentLoaded', () => {
s3secretKey: document.querySelector('#s3-secretKey') as SlInput,
s3region: document.querySelector('#s3-region') as SlInput,
jsonTab: document.querySelector('#json-tab') as SlTab,
mySqlTab: document.querySelector('#mysql-tab') as SlTab,
mySqlHost: document.querySelector('#mysql-host') as SlInput,
mySqlUser: document.querySelector('#mysql-user') as SlInput,
mySqlPassword: document.querySelector('#mysql-password') as SlInput,
mySqlDatabase: document.querySelector('#mysql-database') as SlInput,
pgsqlTab: document.querySelector('#pgsql-tab') as SlTab,
pgsqlHost: document.querySelector('#pgsql-host') as SlInput,
pgsqlPort: document.querySelector('#pgsql-port') as SlInput,
pgsqlUser: document.querySelector('#pgsql-user') as SlInput,
pgsqlPassword: document.querySelector('#pgsql-password') as SlInput,
pgsqlDatabase: document.querySelector('#pgsql-database') as SlInput,
userUsername: document.querySelector('#user-username') as SlInput,
userPassword: document.querySelector('#user-password') as SlInput,
@ -88,15 +98,36 @@ document.addEventListener('DOMContentLoaded', () => {
config.s3.region = Elements.s3region.value;
}
// Append MySQL to config, if specified
if (Elements.mySqlHost.value != null && Elements.mySqlHost.value !== '') {
if (!config.sql) config.sql = {};
config.sql.mySql = {
host: Elements.mySqlHost.value,
user: Elements.mySqlUser.value,
password: Elements.mySqlPassword.value,
database: Elements.mySqlDatabase.value
// Append database to config, if specified
if (Elements.jsonTab.active) {
config.database = {
kind: 'json'
};
} else if (Elements.mySqlTab.active) {
if (Elements.mySqlHost.value != null && Elements.mySqlHost.value != '') {
config.database = {
kind: 'mysql',
options: {
host: Elements.mySqlHost.value,
user: Elements.mySqlUser.value,
password: Elements.mySqlPassword.value,
database: Elements.mySqlDatabase.value
}
};
}
} else if (Elements.pgsqlTab.active) {
if (Elements.pgsqlHost.value != null && Elements.pgsqlHost.value != '') {
config.database = {
kind: 'postgres',
options: {
host: Elements.pgsqlHost.value,
port: parseInt(Elements.pgsqlPort.value),
user: Elements.pgsqlUser.value,
password: Elements.pgsqlPassword.value,
database: Elements.pgsqlDatabase.value
}
};
}
}
// append rate limit config, if specified

@ -53,6 +53,7 @@
"memorystore": "^1.6.7",
"mysql2": "^3.6.2",
"node-vibrant": "^3.1.6",
"pg": "^8.11.3",
"pug": "^3.0.2",
"sharp": "^0.32.6",
"shoelace-fontawesome-pug": "^6.4.3",
@ -68,6 +69,7 @@
"@types/ffmpeg-static": "^3.0.2",
"@types/fs-extra": "^11.0.3",
"@types/luxon": "^3.3.3",
"@types/node": "^20.8.9"
"@types/node": "^20.8.9",
"@types/pg": "^8.10.7"
}
}

@ -77,6 +77,9 @@ dependencies:
node-vibrant:
specifier: ^3.1.6
version: 3.1.6
pg:
specifier: ^8.11.3
version: 8.11.3
pug:
specifier: ^3.0.2
version: 3.0.2
@ -121,6 +124,9 @@ devDependencies:
'@types/node':
specifier: ^20.8.9
version: 20.8.9
'@types/pg':
specifier: ^8.10.7
version: 8.10.7
packages:
@ -1877,6 +1883,14 @@ packages:
dependencies:
undici-types: 5.26.5
/@types/pg@8.10.7:
resolution: {integrity: sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==}
dependencies:
'@types/node': 20.8.9
pg-protocol: 1.6.0
pg-types: 4.0.1
dev: true
/@types/prop-types@15.7.9:
resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==}
dev: false
@ -2185,6 +2199,11 @@ packages:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false
/buffer-writer@2.0.0:
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
engines: {node: '>=4'}
dev: false
/buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
@ -3699,6 +3718,10 @@ packages:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: false
/obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
dev: true
/omggif@1.0.10:
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
dev: false
@ -3721,6 +3744,10 @@ packages:
wrappy: 1.0.2
dev: false
/packet-reader@1.0.0:
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
dev: false
/pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
dev: false
@ -3771,6 +3798,86 @@ packages:
engines: {node: '>=8'}
dev: false
/pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
requiresBuild: true
dev: false
optional: true
/pg-connection-string@2.6.2:
resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==}
dev: false
/pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
/pg-numeric@1.0.2:
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
engines: {node: '>=4'}
dev: true
/pg-pool@3.6.1(pg@8.11.3):
resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==}
peerDependencies:
pg: '>=8.0'
dependencies:
pg: 8.11.3
dev: false
/pg-protocol@1.6.0:
resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==}
/pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
dev: false
/pg-types@4.0.1:
resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==}
engines: {node: '>=10'}
dependencies:
pg-int8: 1.0.1
pg-numeric: 1.0.2
postgres-array: 3.0.2
postgres-bytea: 3.0.0
postgres-date: 2.0.1
postgres-interval: 3.0.0
postgres-range: 1.1.3
dev: true
/pg@8.11.3:
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
dependencies:
buffer-writer: 2.0.0
packet-reader: 1.0.0
pg-connection-string: 2.6.2
pg-pool: 3.6.1(pg@8.11.3)
pg-protocol: 1.6.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
dev: false
/pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
dependencies:
split2: 4.2.0
dev: false
/phin@2.9.3:
resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==}
dev: false
@ -4158,6 +4265,54 @@ packages:
source-map-js: 1.0.2
dev: false
/postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
dev: false
/postgres-array@3.0.2:
resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==}
engines: {node: '>=12'}
dev: true
/postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-bytea@3.0.0:
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
engines: {node: '>= 6'}
dependencies:
obuf: 1.1.2
dev: true
/postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-date@2.0.1:
resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==}
engines: {node: '>=12'}
dev: true
/postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
dependencies:
xtend: 4.0.2
dev: false
/postgres-interval@3.0.0:
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
engines: {node: '>=12'}
dev: true
/postgres-range@1.1.3:
resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==}
dev: true
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
@ -4588,6 +4743,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
dev: false
/sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}

@ -30,6 +30,41 @@ block content
h3.setup-text-item-title Password
sl-input#user-password(type='password' placeholder='the-most-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa')
//- * Database
h2.setup-text-section-header.mt-4 Database
.setup-panel
sl-tab-group
//- * JSON
sl-tab#json-tab(slot='nav' panel='json') JSON
sl-tab-panel(name='json')
| you all good!
//- * MySQL
sl-tab#mysql-tab(slot='nav' panel='mysql') MySQL
sl-tab-panel(name='mysql')
h3.setup-text-item-title Host
sl-input#mysql-host(type='text' placeholder='mysql.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa')
h3.setup-text-item-title User
sl-input#mysql-user(type='text' placeholder='myassql' clearable): sl-icon(slot='prefix' name='fas-user' library='fa')
h3.setup-text-item-title Password
sl-input#mysql-password(type='password' placeholder='super-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa')
h3.setup-text-item-title Database
sl-input#mysql-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa')
//- * PostgreSQL
sl-tab#pgsql-tab(slot='nav' panel='pgsql') PostgreSQL
sl-tab-panel(name='pgsql')
h3.setup-text-item-title Host
sl-input#pgsql-host(type='text' placeholder='postgres.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa')
h3.setup-text-item-title Port
sl-input#pgsql-port(type='number' placeholder='5432' min='1' max='65535' no-spin-buttons clearable): sl-icon(slot='prefix' name='fas-number' library='fa')
h3.setup-text-item-title User
sl-input#pgsql-user(type='text' placeholder='posgrassql' clearable): sl-icon(slot='prefix' name='fas-user' library='fa')
h3.setup-text-item-title Password
sl-input#pgsql-password(type='password' placeholder='super-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa')
h3.setup-text-item-title Database
sl-input#pgsql-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa')
//- * S3
h2.setup-text-section-header.mt-4 S3 #[span.setup-text-optional optional]
.setup-panel
@ -43,18 +78,6 @@ block content
sl-input#s3-secretKey(type='password' placeholder='EF56GH78IJ90KL12' clearable): sl-icon(slot='prefix' name='fas-user-secret' library='fa')
h3.setup-text-item-title Region #[span.setup-text-optional optional]
sl-input#s3-region(type='text' placeholder='us-east' clearable): sl-icon(slot='prefix' name='fas-map-location-dot' library='fa')
//- * MySQL
h2.setup-text-section-header.mt-4 MySQL #[span.setup-text-optional optional]
.setup-panel
h3.setup-text-item-title Host
sl-input#mysql-host(type='text' placeholder='mysql.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa')
h3.setup-text-item-title User
sl-input#mysql-user(type='text' placeholder='myassql' clearable): sl-icon(slot='prefix' name='fas-user' library='fa')
h3.setup-text-item-title Password
sl-input#mysql-password(type='password' placeholder='super-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa')
h3.setup-text-item-title Database
sl-input#mysql-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa')
//- * Rate Limits
h2.setup-text-section-header.mt-4 Rate Limits #[span.setup-text-optional optional]

Loading…
Cancel
Save