diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index 8b452e08..f76f25f3 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -1,8 +1,12 @@ -import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass'; +import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoDBConfiguration } from 'ass'; import fs from 'fs-extra'; import { path } from '@tycrek/joint'; + import { log } from './log.js'; +import { prepareTemplate } from './templates/parser.js'; +import { TemplateError } from './templates/error.js'; +import { DEFAULT_EMBED, validateEmbed } from './embed.js'; const FILEPATH = path.join('.ass-data/userconfig.json'); @@ -58,9 +62,6 @@ const Checkers: UserConfigTypeChecker = { password: basicStringChecker, database: basicStringChecker, port: (val) => numChecker(val) && val >= 1 && val <= 65535 - }, - postgres: { - port: (val) => numChecker(val) && val >= 1 && val <= 65535 } }, @@ -109,18 +110,13 @@ export class UserConfig { // * 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.kind == 'mysql' || config.database.kind == 'postgres' || config.database.kind == 'mongodb') { 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 (!Checkers.sql.mySql.port(config.database.options.port)) throw new Error('Invalid database port'); - 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'); } } @@ -132,6 +128,30 @@ export class UserConfig { if (!Checkers.rateLimit.endpoint(config.rateLimit.api)) throw new Error('Invalid API rate limit configuration'); } + // * the embed + if (config.embed != null) { + try { + for (let part of ['title', 'description', 'sitename'] as ('title' | 'description' | 'sitename')[]) { + if (config.embed[part] != null) { + if (typeof config.embed[part] == 'string') { + config.embed[part] = prepareTemplate(config.embed[part] as string, { + allowIncludeFile: true + }); + } else throw new Error(`Template string for embed ${part} is not a string`); + } else config.embed[part] = DEFAULT_EMBED[part]; + } + + validateEmbed(config.embed); + } catch (err) { + if (err instanceof TemplateError) { + // tlog messes up the formatting + console.error(err.format()); + + throw new Error('Template error'); + } else throw err; + } + } else config.embed = DEFAULT_EMBED; + // All is fine, carry on! return config; } diff --git a/backend/app.ts b/backend/app.ts index 85830199..2156306d 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -15,6 +15,7 @@ import { DBManager } from './sql/database.js'; import { JSONDatabase } from './sql/json.js'; import { MySQLDatabase } from './sql/mysql.js'; import { PostgreSQLDatabase } from './sql/postgres.js'; +import { MongoDBDatabase } from './sql/mongodb.js'; import { buildFrontendRouter } from './routers/_frontend.js'; /** @@ -128,6 +129,9 @@ async function main() { case 'postgres': await DBManager.use(new PostgreSQLDatabase()); break; + case 'mongodb': + await DBManager.use(new MongoDBDatabase()); + break; } } catch (err) { throw new Error(`Failed to configure SQL`); } } else { // default to json database diff --git a/backend/data.ts b/backend/data.ts index 617197c1..3ad6c5a0 100644 --- a/backend/data.ts +++ b/backend/data.ts @@ -15,6 +15,7 @@ type DataSector = 'files' | 'users'; const DBNAMES = { 'mysql': 'MySQL', 'postgres': 'PostgreSQL', + 'mongodb': 'MongoDB', 'json': 'JSON' }; diff --git a/backend/embed.ts b/backend/embed.ts new file mode 100644 index 00000000..ead029fd --- /dev/null +++ b/backend/embed.ts @@ -0,0 +1,32 @@ +import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from 'ass' + +import { TemplateExecutor } from './templates/executor.js'; + +let executor = TemplateExecutor.createExecutor(); + +export const DEFAULT_EMBED: EmbedTemplate = { + sitename: 'ass', + title: '', + description: '' +}; + +// ensures a template is valid +export const validateEmbed = (template: EmbedTemplate) => { + // lets hope this works + let context = executor.createContext(null!, null!); + + executor.validateTemplate(template.title, context); + executor.validateTemplate(template.description, context); + executor.validateTemplate(template.sitename, context); +} + +// cooks up the embed +export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => { + let context = executor.createContext(user, file); + + return { + title: executor.executeTemplate(template.title, context), + description: executor.executeTemplate(template.description, context), + sitename: executor.executeTemplate(template.sitename, context) + }; +}; \ No newline at end of file diff --git a/backend/routers/api.ts b/backend/routers/api.ts index 08bd9d77..5f0f1c84 100644 --- a/backend/routers/api.ts +++ b/backend/routers/api.ts @@ -12,6 +12,7 @@ import { DBManager } from '../sql/database.js'; import { JSONDatabase } from '../sql/json.js'; import { MySQLDatabase } from '../sql/mysql.js'; import { PostgreSQLDatabase } from '../sql/postgres.js'; +import { MongoDBDatabase } from '../sql/mongodb.js'; const router = Router({ caseSensitive: true }); @@ -41,13 +42,16 @@ router.post('/setup', BodyParserJson(), async (req, res) => { case 'postgres': await DBManager.use(new PostgreSQLDatabase()); break; + case 'mongodb': + await DBManager.use(new MongoDBDatabase()); + 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'); @@ -62,10 +66,11 @@ router.post('/setup', BodyParserJson(), async (req, res) => { router.post('/login', rateLimiterMiddleware('login', UserConfig.config?.rateLimit?.login), BodyParserJson(), (req, res) => { const { username, password } = req.body; + // something tells me we shouldnt be using getall here data.getAll('users') .then((users) => { if (!users) throw new Error('Missing users data'); - else return Object.entries(users as AssUser[]) + else return Object.entries(users as AssUser[]) .filter(([_uid, user]: [string, AssUser]) => user.username === username)[0][1]; // [0] is the first item in the filter results, [1] is AssUser }) .then((user) => Promise.all([bcrypt.compare(password, user.password), user])) diff --git a/backend/routers/index.ts b/backend/routers/index.ts index b62aa593..a287a1bd 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -1,4 +1,4 @@ -import { BusBoyFile, AssFile } from 'ass'; +import { BusBoyFile, AssFile, AssUser } from 'ass'; import fs from 'fs-extra'; import bb from 'express-busboy'; @@ -13,6 +13,8 @@ import { random } from '../generators.js'; import { UserConfig } from '../UserConfig.js'; import { getFileS3, uploadFileS3 } from '../s3.js'; import { rateLimiterMiddleware } from '../ratelimit.js'; +import { DBManager } from '../sql/database.js'; +import { DEFAULT_EMBED, prepareEmbed } from '../embed.js'; const router = Router({ caseSensitive: true }); @@ -30,7 +32,7 @@ bb.extend(router, { router.get('/', (req, res) => UserConfig.ready ? res.render('index', { version: App.pkgVersion }) : res.redirect('/setup')); // Upload flow -router.post('/', rateLimiterMiddleware("upload", UserConfig.config?.rateLimit?.upload), async (req, res) => { +router.post('/', rateLimiterMiddleware('upload', UserConfig.config?.rateLimit?.upload), async (req, res) => { // Check user config if (!UserConfig.ready) return res.status(500).type('text').send('Configuration missing!'); @@ -96,7 +98,47 @@ router.post('/', rateLimiterMiddleware("upload", UserConfig.config?.rateLimit?.u } }); -router.get('/:fakeId', (req, res) => res.redirect(`/direct/${req.params.fakeId}`)); +router.get('/:fakeId', async (req, res) => { + if (!UserConfig.ready) res.redirect('/setup'); + + // Get the ID + const fakeId = req.params.fakeId; + + // Get the file metadata + let _data; + try { _data = await DBManager.get('assfiles', fakeId); } + catch (err) { + log.error('Failed to get', fakeId); + console.error(err); + return res.status(500).send(); + } + + if (!_data) return res.status(404).send(); + else { + let meta = _data as AssFile; + let user = await DBManager.get('assusers', meta.uploader) as AssUser | undefined; + + res.render("viewer", { + url: `/direct/${fakeId}`, + uploader: user?.username ?? 'unknown', + size: meta.size, + time: meta.timestamp, + embed: prepareEmbed({ + title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, + description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description, + sitename: UserConfig.config.embed?.sitename ?? DEFAULT_EMBED.sitename + }, user ?? { + admin: false, + files: [], + id: "", + meta: {}, + password: "", + tokens: [], + username: "unknown" + }, meta) + }); + } +}); router.get('/direct/:fakeId', async (req, res) => { if (!UserConfig.ready) res.redirect('/setup'); diff --git a/backend/sql/database.ts b/backend/sql/database.ts index 68f1bb84..f9f22df7 100644 --- a/backend/sql/database.ts +++ b/backend/sql/database.ts @@ -35,7 +35,7 @@ export class DBManager { public static configure(): Promise { if (this._db && this._dbReady) { return this._db.configure(); - } else throw new Error("No database active"); + } else throw new Error('No database active'); } /** @@ -44,7 +44,7 @@ export class DBManager { public static put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise { if (this._db && this._dbReady) { return this._db.put(table, key, data); - } else throw new Error("No database active"); + } else throw new Error('No database active'); } /** @@ -53,7 +53,7 @@ export class DBManager { public static get(table: DatabaseTable, key: NID): Promise { if (this._db && this._dbReady) { return this._db.get(table, key); - } else throw new Error("No database active"); + } else throw new Error('No database active'); } /** @@ -62,6 +62,6 @@ export class DBManager { public static getAll(table: DatabaseTable): Promise { if (this._db && this._dbReady) { return this._db.getAll(table); - } else throw new Error("No database active"); + } else throw new Error('No database active'); } } \ No newline at end of file diff --git a/backend/sql/mongodb.ts b/backend/sql/mongodb.ts new file mode 100644 index 00000000..b0d8eb4c --- /dev/null +++ b/backend/sql/mongodb.ts @@ -0,0 +1,244 @@ +import { AssFile, AssUser, MongoDBConfiguration, NID, UploadToken, Database, DatabaseTable, DatabaseValue } from 'ass'; + +import mongoose, { Model, Mongoose, Schema } from 'mongoose'; + +import { UserConfig } from '../UserConfig.js'; +import { log } from '../log.js'; + +interface TableVersion { + name: string; + version: number; +} + +const VERSIONS_SCHEMA = new Schema({ + name: String, + version: Number +}); + +interface MongoSchema { + id: NID, + data: T +} + +const FILE_SCHEMA = new Schema>({ + id: String, + data: { + fakeid: String, + fileKey: String, + filename: String, + mimetype: String, + save: { + local: String, + s3: Boolean // this will break if it gets the url object + // but im so fucking tired of this, were just + // going to keep it like this until it becomes + // a problem + }, + sha256: String, + size: Number, + timestamp: String, + uploader: String + } +}); + +const TOKEN_SCHEMA = new Schema>({ + id: String, + data: { + id: String, + token: String, + hint: String + } +}); + +const USER_SCHEMA = new Schema>({ + id: String, + data: { + id: String, + username: String, + password: String, + admin: Boolean, + tokens: [ String ], + files: [ String ], + meta: { + type: String, + get: (v: string) => JSON.parse(v), + set: (v: string) => JSON.stringify(v) + } + } +}); + +/** + * database adapter for mongodb + */ +export class MongoDBDatabase implements Database { + private _client: Mongoose; + + // mongoose models + private _versionModel: Model; + private _fileModel: Model>; + private _tokenModel: Model>; + private _userModel: Model>; + + private _validateConfig(): string | undefined { + // make sure the configuration exists + if (!UserConfig.ready) return 'User configuration not ready'; + if (typeof UserConfig.config.database != 'object') return 'MongoDB configuration missing'; + if (UserConfig.config.database.kind != 'mongodb') return 'Database not set to MongoDB, but MongoDB is in use, something has gone terribly wrong'; + if (typeof UserConfig.config.database.options != 'object') return 'MongoDB 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 MongoDB Host' + : !checker(config.user) ? 'Missing MongoDB User' + : !checker(config.password) ? 'Missing MongoDB Password' + : !checker(config.database) ? 'Missing MongoDB Database' + // ! Blame VS Code for this weird indentation + : undefined; + + return issue; + } + + open(): Promise { + return new Promise(async (resolve, reject) => { + try { + // validate config + let configError = this._validateConfig(); + if (configError != null) throw new Error(configError); + + let options = UserConfig.config.database!.options! as MongoDBConfiguration; + + // connect + log.info('MongoDB', `connecting to ${options.host}:${options.port}`); + this._client = await mongoose.connect(`mongodb://${options.user}:${options.password}@${options.host}:${options.port}/${options.database}`); + log.success('MongoDB', 'ok'); + + resolve(); + } catch (err) { + log.error('MongoDB', 'failed to connect'); + console.error(err); + reject(err); + } + }); + } + + close(): Promise { + return new Promise(async (resolve, reject) => { + try { + // gracefully disconnect + await this._client.disconnect(); + + resolve(); + } catch (err) { + log.error('MongoDB', 'failed to disconnect'); + console.error(err); + reject(err); + } + }); + } + + configure(): Promise { + return new Promise(async (resolve, reject) => { + try { + this._versionModel = this._client.model('assversions', VERSIONS_SCHEMA); + this._fileModel = this._client.model('assfiles', FILE_SCHEMA); + this._tokenModel = this._client.model('asstokens', TOKEN_SCHEMA); + this._userModel = this._client.model('assusers', USER_SCHEMA); + + // theres only one version right now so we dont need to worry about anything, just adding the version thingies if they arent there + let versions = await this._versionModel.find().exec() + .then(res => res.reduce((obj, doc) => obj.set(doc.name, doc.version), new Map())); + + for (let [table, version] of [['assfiles', 1], ['asstokens', 1], ['assusers', 1]] as [string, number][]) { + if (!versions.has(table)) { + // set the version + new this._versionModel({ + name: table, + version: version + }).save(); + + versions.set(table, version); + } + } + + resolve(); + } catch (err) { + log.error('MongoDB', 'failed to configure'); + console.error(err); + reject(err); + } + }); + } + + put(table: DatabaseTable, key: string, data: DatabaseValue): Promise { + return new Promise(async (resolve, reject) => { + try { + const models = { + assfiles: this._fileModel, + assusers: this._userModel, + asstokens: this._tokenModel + }; + + await new models[table]({ + id: key, + data: data + }).save(); + + resolve(); + } catch (err) { + reject(err); + } + }); + } + + get(table: DatabaseTable, key: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const models = { + assfiles: this._fileModel, + assusers: this._userModel, + asstokens: this._tokenModel + }; + + // @ts-ignore + // typescript cant infer this but it is 100% correct + // no need to worry :> + let result = await models[table].find({ + id: key + }).exec(); + + if (result.length == 0) { + throw new Error(`Key '${key}' not found in '${table}'`); + } + + resolve(result.length ? result[0].data : void 0); + } catch (err) { + reject(err); + } + }); + } + + // TODO: Unsure if this works. + getAll(table: DatabaseTable): Promise { + return new Promise(async (resolve, reject) => { + try { + const models = { + assfiles: this._fileModel, + assusers: this._userModel, + asstokens: this._tokenModel + }; + + // more ts-ignore! + // @ts-ignore + let result = await models[table].find({}).exec() // @ts-ignore + .then(res => res.reduce((obj, doc) => (obj.push(doc.data)), [])); + + resolve(result); + } catch (err) { + reject(err); + } + }); + } +}; \ No newline at end of file diff --git a/backend/sql/mysql.ts b/backend/sql/mysql.ts index f24219c9..8ae04bdd 100644 --- a/backend/sql/mysql.ts +++ b/backend/sql/mysql.ts @@ -31,7 +31,7 @@ export class MySQLDatabase implements Database { // 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 (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; diff --git a/backend/sql/postgres.ts b/backend/sql/postgres.ts index 1dfe8753..0ea1d544 100644 --- a/backend/sql/postgres.ts +++ b/backend/sql/postgres.ts @@ -17,7 +17,7 @@ export class PostgreSQLDatabase implements Database { // 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 (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; @@ -33,7 +33,6 @@ export class PostgreSQLDatabase implements Database { : undefined; return issue; - } public open(): Promise { @@ -172,26 +171,30 @@ export class PostgreSQLDatabase implements Database { let result = await this._client.query(queries[table], [key]); - resolve(result.rowCount ? result.rows[0].data : void 0); + if (result.rowCount == 0) { + throw new Error(`Key '${key}' not found in '${table}'`); + } + + resolve(result.rows[0].data); } catch (err) { reject(err); } }); } - // todo: verify this works + // XXX: This is broken public getAll(table: DatabaseTable): Promise { 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;' + assfiles: 'SELECT json_agg(data) as stuff FROM assfiles;', + assusers: 'SELECT json_agg(data) as stuff FROM assusers;', + asstokens: 'SELECT json_agg(data) as stuff FROM asstokens;' }; let result = await this._client.query(queries[table]); - resolve(result.rowCount ? result.rows[0].stuff : void 0); + resolve(result.rowCount ? result.rows[0].stuff : []); } catch (err) { reject(err); } diff --git a/backend/templates/command.ts b/backend/templates/command.ts new file mode 100644 index 00000000..eb2f6514 --- /dev/null +++ b/backend/templates/command.ts @@ -0,0 +1,10 @@ +import { TemplateCommandOp, TemplateCommandSchema } from 'ass'; + +import { TemplateContext } from './executor.js'; + +export type TemplateCommand = { + readonly name: N; + readonly schema: S; + + exec: (op: TemplateCommandOp, ctx: TemplateContext) => string; +}; \ No newline at end of file diff --git a/backend/templates/error.ts b/backend/templates/error.ts new file mode 100644 index 00000000..9956af67 --- /dev/null +++ b/backend/templates/error.ts @@ -0,0 +1,81 @@ +import { TemplateSourceRange } from 'ass'; + +export class TemplateError extends Error { + range?: TemplateSourceRange; + + constructor(msg: string, range?: TemplateSourceRange) { + super(msg); + + this.range = range; + } + + /** + * Formats the error. + */ + public format(): string { + let format = ''; + + if (this.range) { + let fcol = 0; + let fline = 1; + let pstart = 0; + + for (let i = 0; i < this.range.from; i++) { + fcol++; + if (this.range.file.code[i] == '\n') { + fline++; + fcol = 0; + pstart = i + 1; + } + } + + let tcol = fcol; + let tline = fline; + let pend = pstart; + + for (let i = this.range.from; i < this.range.to; i++) { + tcol++; + if (this.range.file.code[i] == '\n') { + tline++; + tcol = 0; + pend = i + 1; + } + } + + if ((pend = this.range.file.code.indexOf('\n', pend)) == -1) { + pend = this.range.file.code.length - 1; + } + + if (fline == tline) { + format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend + 1)}\n`; + format += `${fline.toString().padStart(5, ' ')} | ${' '.repeat(fcol)}^${'~'.repeat(Math.max(tcol - fcol, 0))}\n`; + } else { + let lines = this.range.file.code.substring(pstart, pend + 1).split('\n'); + + format += ` | /${'~'.repeat(lines[0].length)}v\n`; + + for (let i = fline; i < fline + 5 && i <= tline; i++) { + format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`; + } + + if (fline + 5 < tline) { + format += ` | | ...\n`; + + for (let i = tline - 4; i <= tline; i++) { + format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`; + } + } + + format += ` | \\${'~'.repeat(tcol + 1)}^\n`; + } + } + + format += `${this.name}: ${this.message}`; + + return format; + } +} + +// template syntax error with token range, token range converted to source position +// outside of prepareTemplate +export class TemplateSyntaxError extends TemplateError {}; \ No newline at end of file diff --git a/backend/templates/executor.ts b/backend/templates/executor.ts new file mode 100644 index 00000000..de321600 --- /dev/null +++ b/backend/templates/executor.ts @@ -0,0 +1,122 @@ +import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass'; + +import { TemplateCommand } from './command.js'; +import { TemplateError } from './error.js'; + +export class TemplateContext { + public readonly owner: TemplateExecutor; + + public uploader: AssUser; + public file: AssFile; + + constructor(owner: TemplateExecutor, uploader: AssUser, file: AssFile) { + this.owner = owner; + this.uploader = uploader; + this.file = file; + } +} + +export class TemplateExecutor { + private commands: { [index: string]: TemplateCommand } = {}; + + // register a template command globally + public registerCommand(name: N, attrs: S, cmd: (op: TemplateCommandOp, ctx: TemplateContext) => string) { + if (this.commands[name] == null) { + this.commands[name] = { + name: name, + schema: attrs, + exec: cmd + }; + } else throw new Error(`Template command "${name}" already exists`); + } + + public createContext(uploader: AssUser, file: AssFile) { + return new TemplateContext(this, uploader, file); + } + + // expects template to be valid and does not preform runtime checks. + // run validateTemplate first + public executeTemplate(op: TemplateOp, ctx: TemplateContext): string { + switch (typeof op) { + case 'string': return op; + case 'object': return this.commands[op.op].exec(op, ctx); + } + } + + public validateTemplate(op: TemplateOp, ctx: TemplateContext): void { + if (typeof op == 'string') return; + + if (this.commands[op.op] != null) { + let cmd = this.commands[op.op].schema as TemplateCommandSchema; + + if (cmd.named) { + for (let name in cmd.named) { + let arg = cmd.named[name]; + + // @ts-ignore + if (arg.required && op.named[name] == null) { + throw new TemplateError(`Required template argument "${name}" is missing.`, op.srcRange); + } + } + } + + for (let arg of op.args) this.validateTemplate(arg, ctx); + for (let name in op.named) { + if (!cmd.named || cmd.named[name] == null) { + let arg = (op.named as {[index:string]:TemplateOp})[name] as TemplateOp | undefined; + + // @ts-ignore + throw new TemplateError(`Unknown template argument "${name}".`, { + file: op.srcRange.file, + from: (typeof arg == 'object' && arg.srcRange.from - 1 - name.length) || op.srcRange.from, + to: (typeof arg == 'object' && arg.srcRange.to) || op.srcRange.to + }); + } + + // @ts-ignore + this.validateTemplate(op.named[name]!, ctx); + } + } else throw new TemplateError(`Template command "${op.op}" does not exist.`, op.srcRange); + } + + // creates an executor with the default commands. + public static createExecutor(): TemplateExecutor { + let ex = new TemplateExecutor(); + + // joins two strings + ex.registerCommand('concat', {}, (op, ctx) => { + return op.args.reduce((a: string, b): string => a + ctx.owner.executeTemplate(b, ctx), ""); + }); + + // converts a number to a file size + ex.registerCommand('formatbytes', { + named: { unit: {} } + }, (op, ctx) => { + let value = ctx.owner.executeTemplate(op.args[0], ctx); + + let exponent = (op.named.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[ctx.owner.executeTemplate(op.named.unit, ctx)]) + || Math.max(Math.min(Math.floor(Math.log10(Number(value))/3), 4), 0); + + return `${(Number(value) / 1000 ** exponent).toFixed(2)}${['b', 'kb', 'mb', 'gb', 'tb'][exponent]}`; + }); + + // gets the size of the active file + ex.registerCommand('filesize', {}, (op, ctx) => { + return ctx.file.size.toString(); + }); + + // gets the uploader of the active file + ex.registerCommand('uploader', {}, (op, ctx) => { + return ctx.uploader.username; + }); + + // selects a random argument + ex.registerCommand('random', {}, (op, ctx) => { + if (op.args.length > 0) { + return ctx.owner.executeTemplate(op.args[Math.round(Math.random() * (op.args.length - 1))], ctx); + } else throw new TemplateError('Random without arguments'); + }); + + return ex; + } +} \ No newline at end of file diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts new file mode 100644 index 00000000..cd4d5e49 --- /dev/null +++ b/backend/templates/parser.ts @@ -0,0 +1,305 @@ +import { TemplateOp, TemplateSource } from 'ass'; + +import fs from 'fs'; + +import { TemplateSyntaxError } from './error.js'; + +enum TokenType { + T_OPEN, T_CLOSE, + PIPE, EQUALS, + TEXT, +}; + +type TemplateToken = { + type : TokenType; + data?: string; + from: number; + to: number; +}; + +// tree used by findReplacement to select the best amp-substitution +type TemplateAmpNode = { [index: string]: TemplateAmpNode | string | undefined; } +const TEMPLATE_AMP_SUBSTITUTIONS: TemplateAmpNode = { + e: { q: { $: '=' } }, + p: { i: { p: { e: { $: '|' } } } }, + t: { + c: { l: { o: { s: { e: { $: '}}' } } } } }, + o: { p: { e: { n: { $: '{{' } } } } + } +}; + +function getTemplateTokens(src: string): TemplateToken[] { + let tokens: TemplateToken[] = []; + let buf : string = ''; + let pos : number = 0; + + // digs through TEMPLATE_AMP_SUBSTITUTIONS to find + // longest possible string to replace + function findReplacement() { + let raw = ""; + let bestpos: number | null = null; + let best: string | null = null; + let node = TEMPLATE_AMP_SUBSTITUTIONS; + + while (true) { + if (pos >= src.length) break; + if (!/[a-z]/.test(src[pos])) break; + + if (node[src[pos]] != null) { // enter the thing + node = node[src[pos]] as TemplateAmpNode; + } else { + break; + } + + if (node.$ != null) { + best = node.$ as string; + bestpos = pos; + } + + raw += src[pos++]; + } + + if (best != null) { + pos = bestpos! + 1; + return best; + } + + return `&${raw}`; + } + + for (; pos < src.length; pos++) { + let lp = pos; + + if (pos + 1 < src.length && src[pos] == '{' && src[pos + 1] == '{') { + tokens.push({ + type: TokenType.T_OPEN, + from: pos, + to: pos + 1 + }); + pos++; + } else if (pos + 1 < src.length && src[pos] == '}' && src[pos + 1] == '}') { + tokens.push({ + type: TokenType.T_CLOSE, + from: pos, + to: pos + 1 + }); + pos++; + } else if (src[pos] == '|') { + tokens.push({ + type: TokenType.PIPE, + from: pos, + to: pos + }); + } else if (src[pos] == '=') { + tokens.push({ + type: TokenType.EQUALS, + from: pos, + to: pos + }); + } else if (src[pos] == '&') { + pos++; + buf += findReplacement(); + pos--; continue; + } else if (src[pos] == '\n') { + pos++; + for (; pos < src.length && (src[pos] == ' ' || src[pos] == '\t'); pos++); + pos--; continue; + } else { + buf += src[pos]; + continue; + } + + if (buf.length) { + tokens.splice(-1, 0, { + type: TokenType.TEXT, + data: buf, + from: lp - buf.length, + to: lp - 1 + }); + buf = ''; + } + } + + if (buf.length) tokens.push({ + type: TokenType.TEXT, + data: buf, + from: src.length - buf.length, + to: src.length + }); + + return tokens; +} + +export type PrepareTemplateOptions = { + allowIncludeFile?: boolean; +}; + +export function prepareTemplate(src: string, config?: PrepareTemplateOptions): TemplateOp { + let options = { + includeFiles: config?.allowIncludeFile ?? false + }; + + type ParserStackEntry = { + pos: number + }; + + let tokens = getTemplateTokens(src); + let pos = 0; + + let stack: ParserStackEntry[] = []; + + function stackPush() { + stack.push({ pos: pos }); + } + + function stackDrop() { + stack.pop(); + } + + let file: TemplateSource = { code: src }; + + // parse the text part of stuff. like uh + // you know uh like uh this part + // V---V V V-V V + // Hello {Concat|W|ORL|D} + function parseConcat(root: boolean = false): TemplateOp { + let joined: TemplateOp[] = []; + + let start = pos; + + stackPush(); + + out: while (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.TEXT: + joined.push(tokens[pos++].data!); + continue out; + case TokenType.EQUALS: + if (root == true) throw new TemplateSyntaxError('Unexpected equals', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + case TokenType.PIPE: + if (root == true) throw new TemplateSyntaxError('Unexpected pipe', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + case TokenType.T_CLOSE: + if (root == true) throw new TemplateSyntaxError('Unexpected closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + break out; + case TokenType.T_OPEN: + joined.push(parseTemplate()); + } + } + + stackDrop(); + + return joined.length == 1 ? joined[0] : { + op: "concat", + named: {}, + args: joined, + srcRange: { + file: file, + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 + } + }; + } + + // parse templates + function parseTemplate(): TemplateOp { + let name: string; + let args: TemplateOp[] = []; + let nargs: {[index: string]: TemplateOp} = {}; + let start = pos; + + stackPush(); + + if (pos < tokens.length && tokens[pos].type == TokenType.T_OPEN) { + pos++; + } else throw new Error('Catastrophic failure'); + + if (pos < tokens.length && tokens[pos].type == TokenType.TEXT) { + name = tokens[pos++].data!; + } else if (pos < tokens.length) { + if (tokens[pos].type == TokenType.T_CLOSE) { + throw new TemplateSyntaxError('Template name missing', { file: file, from: tokens[pos - 1].from, to: tokens[pos].to }); + } else throw new TemplateSyntaxError('Expected template name', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + } else throw new TemplateSyntaxError('Unexpected end of file'); + + if (pos < tokens.length && tokens[pos].type == TokenType.PIPE) { + pos++; + + out: while (pos < tokens.length) { + let argStart = pos; + + let arg = parseConcat(); + + // this is some really nasty control flow im so sorry + if (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.EQUALS: // named arguments + if (typeof arg != 'string') { + throw new TemplateSyntaxError('Argument name must be a plain string', { file: file, from: tokens[argStart].from, to: tokens[pos - 1].to }); + } + + pos++; + if (pos < tokens.length) { + let arg2 = parseConcat(); + nargs[arg] = arg2; + if (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.T_CLOSE: break out; + case TokenType.PIPE: pos++; + } + } else throw new TemplateSyntaxError('syntax error'); + } else throw new TemplateSyntaxError('syntax error'); + break; + case TokenType.T_CLOSE: + args.push(arg); + break out; + case TokenType.PIPE: + args.push(arg); + pos++; + } + } + } + } else if (pos < tokens.length && tokens[pos].type != TokenType.T_CLOSE) { + throw new TemplateSyntaxError('Expected arguments or closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + } + + if (pos < tokens.length && tokens[pos].type == TokenType.T_CLOSE) { + pos++; + } else throw new TemplateSyntaxError('Template closing tag missing'); + + stackDrop(); + + // include is executed early + if (name.toLowerCase() == 'include') { + if (nargs['file'] != null) { + // security check! + if (!options.includeFiles) { + throw new TemplateSyntaxError('You are not allowed to include files', { file: file, from: tokens[start].from, to: tokens[pos - 1].to }); + } + + if (typeof nargs['file'] == 'string') { + if (fs.existsSync(nargs['file'])) { + let template = fs.readFileSync(nargs['file'], { encoding: 'utf-8' }); + + let tl = prepareTemplate(template, config); + + return tl; + } else throw new TemplateSyntaxError('File does not exist', { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } else throw new TemplateSyntaxError('Include directive can not contain templates', { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } else throw new TemplateSyntaxError(`Bad include directive`, { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } + + return { + op: name.toLocaleLowerCase(), + named: nargs, + args: args, + srcRange: { + file: file, + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 + } + }; + } + + let result = parseConcat(true); + return result; +} \ No newline at end of file diff --git a/common/types.d.ts b/common/types.d.ts index 299b1ad6..60bbac18 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -30,6 +30,25 @@ declare module 'ass' { database?: DatabaseConfiguration; rateLimit?: RateLimitConfiguration; + + // to whoever has to make the config screen + // for this, im very verys sorry + embed?: EmbedTemplate; + } + + /** + * Embed config + */ + interface EmbedConfiguration { + /** + * Title in embed + */ + title?: string, + + /** + * Description(s) in embed + */ + description?: string[] | string, } interface S3Configuration { @@ -91,8 +110,8 @@ declare module 'ass' { } interface DatabaseConfiguration { - kind: 'mysql' | 'postgres' | 'json'; - options?: MySQLConfiguration | PostgresConfiguration; + kind: 'mysql' | 'postgres' | 'json' | 'mongodb'; + options?: MySQLConfiguration | PostgresConfiguration | MongoDBConfiguration; } interface MySQLConfiguration { @@ -111,6 +130,14 @@ declare module 'ass' { database: string; } + interface MongoDBConfiguration { + host: string; + port: number; + user: string; + password: string; + database: string; + } + /** * rate limiter configuration * @since 0.15.0 @@ -171,9 +198,6 @@ declare module 'ass' { password: (val: any) => boolean; database: (val: any) => boolean; } - postgres: { - port: (val: any) => boolean; - } } rateLimit: { endpoint: (val: any) => boolean; @@ -288,6 +312,77 @@ declare module 'ass' { cliKey: string; meta: { [key: string]: any }; } + + /** + * Template operation + */ + type TemplateOp = TemplateCommandOp | string; + + /** + * Please don't waste your time trying to make this look + * nice, it's not possible. + */ + type TemplateCommandOp = { + op: N; + args: TemplateOp[]; + named: { + +readonly [name in keyof T['named']]: ( + TemplateOp | (T['named'] extends object + ? T['named'][name] extends { required?: boolean } + ? T['named'][name]['required'] extends true + ? TemplateOp + : undefined + : undefined + : undefined) + ) + }; + srcRange: TemplateSourceRange; + }; + + /** + * Basically a declaration + */ + type TemplateCommandSchema = { + named?: { + [index: string]: { + required?: boolean + } + }; + }; + + /** + * Template source code + */ + type TemplateSource = { + code: string; + }; + + /** + * Range in template source code + */ + type TemplateSourceRange = { + file: TemplateSource; + from: number; + to: number; + }; + + /** + * This is so beyond cursed + */ + interface EmbedTemplate { + title: TemplateOp; + description: TemplateOp; + sitename: TemplateOp; + } + + /** + * + */ + interface PreparedEmbed { + title: string; + description: string; + sitename: string; + } } //#region Dummy modules diff --git a/frontend/setup.mts b/frontend/setup.mts index 25e730f7..5c9d962d 100644 --- a/frontend/setup.mts +++ b/frontend/setup.mts @@ -57,6 +57,13 @@ document.addEventListener('DOMContentLoaded', () => { pgsqlPassword: document.querySelector('#pgsql-password') as SlInput, pgsqlDatabase: document.querySelector('#pgsql-database') as SlInput, + mongoDBTab: document.querySelector('#mongodb-tab') as SlTab, + mongoDBHost: document.querySelector('#mongodb-host') as SlInput, + mongoDBPort: document.querySelector('#mongodb-port') as SlInput, + mongoDBUser: document.querySelector('#mongodb-user') as SlInput, + mongoDBPassword: document.querySelector('#mongodb-password') as SlInput, + mongoDBDatabase: document.querySelector('#mongodb-database') as SlInput, + userUsername: document.querySelector('#user-username') as SlInput, userPassword: document.querySelector('#user-password') as SlInput, @@ -130,6 +137,19 @@ document.addEventListener('DOMContentLoaded', () => { } }; } + } else if (Elements.mongoDBTab.active) { + if (Elements.mongoDBHost.value != null && Elements.mongoDBHost.value != '') { + config.database = { + kind: 'mongodb', + options: { + host: Elements.mongoDBHost.value, + port: parseInt(Elements.mongoDBPort.value), + user: Elements.mongoDBUser.value, + password: Elements.mongoDBPassword.value, + database: Elements.mongoDBDatabase.value + } + }; + } } // append rate limit config, if specified diff --git a/package.json b/package.json index 645f378a..82d52b18 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "fs-extra": "^11.2.0", "luxon": "^3.4.4", "memorystore": "^1.6.7", + "mongoose": "^8.0.0", "mysql2": "^3.6.5", "node-vibrant": "^3.1.6", "pg": "^8.11.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a80aa02f..2af52f80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: memorystore: specifier: ^1.6.7 version: 1.6.7 + mongoose: + specifier: ^8.0.0 + version: 8.0.0 mysql2: specifier: ^3.6.5 version: 3.6.5 @@ -1862,6 +1865,12 @@ packages: - supports-color dev: false + /@mongodb-js/saslprep@1.1.1: + resolution: {integrity: sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==} + dependencies: + sparse-bitfield: 3.0.3 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2898,6 +2907,17 @@ packages: - vue dev: true + /@types/webidl-conversions@7.0.2: + resolution: {integrity: sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==} + dev: false + + /@types/whatwg-url@8.2.2: + resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} + dependencies: + '@types/node': 20.8.9 + '@types/webidl-conversions': 7.0.2 + dev: false + /@xoi/gps-metadata-remover@1.1.2(@babel/core@7.23.5): resolution: {integrity: sha512-QeGcEvlesS+cXwfao14kdLI2zHJk3vppKSEbpbiNP1abx45P8HWqGEWhgF71bKlnCSW8a7b4RNDNa4mj1aHPMA==} dependencies: @@ -3195,6 +3215,11 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.22.2) dev: false + /bson@6.2.0: + resolution: {integrity: sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==} + engines: {node: '>=16.20.1'} + dev: false + /buffer-equal@0.0.1: resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} engines: {node: '>=0.4.0'} @@ -4557,6 +4582,11 @@ packages: promise: 7.3.1 dev: false + /kareem@2.5.1: + resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==} + engines: {node: '>=12.0.0'} + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4707,6 +4737,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + dev: false + /memorystore@1.6.7: resolution: {integrity: sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==} engines: {node: '>=0.10'} @@ -4875,11 +4909,86 @@ packages: hasBin: true dev: false + /mongodb-connection-string-url@2.6.0: + resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==} + dependencies: + '@types/whatwg-url': 8.2.2 + whatwg-url: 11.0.0 + dev: false + + /mongodb@6.2.0: + resolution: {integrity: sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + dependencies: + '@mongodb-js/saslprep': 1.1.1 + bson: 6.2.0 + mongodb-connection-string-url: 2.6.0 + dev: false + + /mongoose@8.0.0: + resolution: {integrity: sha512-PzwkLgm1Jhj0NQdgGfnFsu0QP9V1sBFgbavEgh/IPAUzKAagzvEhuaBuAQOQGjczVWnpIU9tBqyd02cOTgsPlA==} + engines: {node: '>=16.20.1'} + dependencies: + bson: 6.2.0 + kareem: 2.5.1 + mongodb: 6.2.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 16.0.1 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + dev: false + + /mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + dev: false + + /mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /mrmime@1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} dev: true - + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -5805,6 +5914,11 @@ packages: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: false + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false + /qr-creator@1.0.0: resolution: {integrity: sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==} dev: false @@ -6124,6 +6238,10 @@ packages: object-inspect: 1.13.1 dev: false + /sift@16.0.1: + resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==} + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false @@ -6171,6 +6289,12 @@ packages: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} dev: true + /sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + dependencies: + memory-pager: 1.5.0 + dev: false + /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -6453,10 +6577,13 @@ packages: ieee754: 1.2.1 dev: false - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 dev: false - + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: true @@ -6772,6 +6899,19 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: diff --git a/tailwind.css b/tailwind.css index 6e627578..1e87c7eb 100644 --- a/tailwind.css +++ b/tailwind.css @@ -24,6 +24,15 @@ .setup-panel>sl-input { @apply mb-4; } + + .res-image { + @apply max-h-[75vh]; + } + + /* THANKS TAILWIND */ + sl-divider { + border: solid var(--width) var(--color); + } } @layer utilities { diff --git a/views/setup.pug b/views/setup.pug index e203feea..edf79255 100644 --- a/views/setup.pug +++ b/views/setup.pug @@ -67,6 +67,20 @@ block content h3.setup-text-item-title Database sl-input#pgsql-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa') + //- * MongoDB + sl-tab#mongodb-tab(slot='nav' panel='mongodb') MongoDB + sl-tab-panel(name='mongodb') + h3.setup-text-item-title Host + sl-input#mongodb-host(type='text' placeholder='mongo.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa') + h3.setup-text-item-title Port + sl-input#mongodb-port(type='number' placeholder='27017' 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#mongodb-user(type='text' placeholder='mongo' clearable): sl-icon(slot='prefix' name='fas-user' library='fa') + h3.setup-text-item-title Password + sl-input#mongodb-password(type='password' placeholder='super-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa') + h3.setup-text-item-title Database + sl-input#mongodb-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 diff --git a/views/viewer.pug b/views/viewer.pug new file mode 100644 index 00000000..84bf35d9 --- /dev/null +++ b/views/viewer.pug @@ -0,0 +1,60 @@ +doctype html +html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') + head + //- this stuff + meta(charset='UTF-8') + meta(name='viewport', content='width=device-witdh, initial-scale=1.0') + meta(name='theme-color' content='black') + link(rel='stylesheet' href='/.css') + + //- title + title ass 🍑 + + //- embed data + meta(property='og:title', content=embed.title) + meta(property='og:description', content=embed.description) + meta(property='og:site_name', content=embed.sitename) + meta(property='og:type', content='image') + meta(property='og:image', content=url) + meta(property='og:url', content='.') + meta(property='twitter:card', content='summary_large_image') + + //- mixins + include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug + include ../node_modules/shoelace-pug-loader/loader.pug + + //- shoelace + +slTheme('dark') + +slAuto + body.w-screen.h-screen.flex-col + div.w-full.h-full.flex.justify-center.items-center.text-center + main.flex.flex-col + - let borderStyle = { 'border-color': 'var(--sl-color-neutral-200)' }; + header.border-t.border-x.flex.h-8.bg-stone-900(style=borderStyle) + - let dividerStyle = { '--spacing': '0px', 'margin-left': '8px' }; + + //- uploader + span + sl-icon.p-2.align-middle(name='person-fill', label='uploader') + | #{uploader} + sl-divider(vertical, style=dividerStyle) + //- file size + span + sl-icon.p-2.align-middle(name='database-fill', label='size') + sl-format-bytes(value=size) + sl-divider(vertical, style=dividerStyle) + //- upload date + span + //- calendar is a funny word + sl-icon.p-2.align-middle(name='calendar-fill', label='upload date') + sl-format-date(value=time) + sl-divider(vertical, style=dividerStyle) + + //- spacer + div.flex-grow + + //- download button + sl-divider(vertical, style=dividerStyle) + span.float-right + sl-icon-button(name='download', href=url, download, label='download') + img.res-image.border-b.border-x(src=url, style=borderStyle) \ No newline at end of file