From e6b1e72ef7558c318e8240991be7fa12d2e5e8c1 Mon Sep 17 00:00:00 2001 From: Facinorous Date: Tue, 29 Nov 2022 18:45:15 -0500 Subject: [PATCH 01/59] - Fix txtPrimary & txtSecondary class positions - Add linkPrimary class for link colour --- .github/README.md | 1 + tailwind.config.js | 3 ++- tailwind.css | 2 +- views/view.pug | 10 +++++----- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/README.md b/.github/README.md index e179b447..31354bc8 100644 --- a/.github/README.md +++ b/.github/README.md @@ -290,6 +290,7 @@ If you want to customize the font or colours of the viewer page, create a file i | **`bgViewer`** | Background colour for the viewer element | | **`txtPrimary`** | Primary text colour; this should be your main brand colour. | | **`txtSecondary`** | Secondary text colour; this is used for the file details. | +| **`linkPrimary`** | Primary link colour | | **`linkHover`** | Colour of the `hover` effect for links | | **`linkActive`** | Colour of the `active` effect for links | | **`borderHover`** | Colour of the `hover` effect for borders; this is used for the underlining links. | diff --git a/tailwind.config.js b/tailwind.config.js index fa3e321f..d3d380dc 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -18,10 +18,11 @@ const defaults = { bgViewer: '#151515', // Text colours - txtPrimary: '#FD842D', + txtPrimary: '#BDBDBD', txtSecondary: '#BDBDBD', // Links + linkPrimary: '#FD842D', linkHover: '#FD710D', linkActive: '#DE5E02', diff --git a/tailwind.css b/tailwind.css index adb200cd..319e20e9 100644 --- a/tailwind.css +++ b/tailwind.css @@ -13,7 +13,7 @@ @apply no-underline hover_no-underline active_no-underline visited_no-underline /* regular, visited */ - text-primary visited_text-primary + text-link visited_text-link border-b-2 visited_border-b-2 border-transparent visited_border-transparent rounded-sm visited_rounded-sm diff --git a/views/view.pug b/views/view.pug index c6aefa50..f0a6725e 100644 --- a/views/view.pug +++ b/views/view.pug @@ -21,10 +21,10 @@ html * { display: none !important; } meta(http-equiv='refresh' content=`0; url='${resourceAttr.src}'`) - body.font-main.text-secondary.bg-page + body.font-main.bg-page .w-full.h-full.flex.justify-center.items-center.text-center .bg-viewer.rounded-24 - h4.mx-4.mt-6.mb-4.text-3xl.font-main!=title + h4.mx-4.mt-6.mb-4.text-3xl.font-main.text-primary!=title figure.block.mx-10.my-4.flex.flex-col.align-items-center if fileIs.video video.res-media(controls loop muted playsinline preload='metadata')&attributes(resourceAttr) @@ -36,10 +36,10 @@ html code!=mimetype figcaption br - span.text-2xl Uploaded by #[strong!=uploader] + span.text-2xl.text-primary Uploaded by #[strong!=uploader] br - span #{timestamp} (#{size}) + span.text-secondary #{timestamp} (#{size}) br span: a.link(href='#' onclick=`window.location = '${resourceAttr.src}?download=yes'; return false;` download=title) Download if showAd - .mx-4.mb-8.text-footer: p Image hosted by #[a.link(href='https://github.com/tycrek/ass' target='_blank'): strong ass], the superior self-hosted ShareX server + .mx-4.mb-8.text-footer.text-secondary: p Image hosted by #[a.link(href='https://github.com/tycrek/ass' target='_blank'): strong ass], the superior self-hosted ShareX server From 62b2bf953fea0b3f208d5c780f2e64290ccc0e8b Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:55:23 -0700 Subject: [PATCH 02/59] feat: added initial API routers (no routes yet) --- src/routers/api.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/routers/api.ts diff --git a/src/routers/api.ts b/src/routers/api.ts new file mode 100644 index 00000000..d1c5765e --- /dev/null +++ b/src/routers/api.ts @@ -0,0 +1,22 @@ +/** + * Developer API + * - Users + * - Resources + */ + +import { Router, Request, Response, NextFunction } from 'express'; +import { users } from '../auth'; +import { data } from '../data'; + +const RouterUser = Router(); +const RouterResource = Router(); + +/** + * Token authentication middleware + */ +const authMiddleware = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization; + (token && users[token]) + ? next() + : res.sendStatus(401); +}; From f302f56ac6319dbbbbaef812917d344e0a33935e Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 15:53:06 -0700 Subject: [PATCH 03/59] feat: gave API an onStart() --- src/ass.ts | 4 ++++ src/routers/api.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/ass.ts b/src/ass.ts index 5b41b6bc..445e893a 100644 --- a/src/ass.ts +++ b/src/ass.ts @@ -10,6 +10,7 @@ import tailwindcss from 'tailwindcss'; import helmet from 'helmet'; import { path, log, getTrueHttp, getTrueDomain } from './utils'; +import { onStart as ApiOnStart } from './routers/api'; //#endregion //#region Setup - Run first time setup if using Docker (pseudo-process, setup will be run with docker exec) @@ -105,6 +106,9 @@ ASS_FRONTEND.enabled && app.use(ASS_FRONTEND.endpoint, ASS_FRONTEND.router); // // Upload router (has to come after custom frontends as express-busboy interferes with all POST calls) app.use('/', ROUTERS.upload); +// API +app.use('/api', ApiOnStart()); + // CSS app.use('/css', epcss({ cssPath: path('tailwind.css'), diff --git a/src/routers/api.ts b/src/routers/api.ts index d1c5765e..c0281302 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -8,6 +8,7 @@ import { Router, Request, Response, NextFunction } from 'express'; import { users } from '../auth'; import { data } from '../data'; +const RouterApi = Router(); const RouterUser = Router(); const RouterResource = Router(); @@ -20,3 +21,10 @@ const authMiddleware = (req: Request, res: Response, next: NextFunction) => { ? next() : res.sendStatus(401); }; + +export const onStart = () => { + RouterApi.use('/user', RouterUser); + RouterApi.use('/resource', RouterResource); + + return RouterApi; +}; From e425425749122033f074973cefc4216f9603db73 Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 21:48:21 -0700 Subject: [PATCH 04/59] build: added nanoid package --- package-lock.json | 1 + package.json | 1 + src/generators/nanoid.ts | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 src/generators/nanoid.ts diff --git a/package-lock.json b/package-lock.json index 6ed19be0..c9c5f99d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "fs-extra": "^10.0.1", "helmet": "^4.6.0", "luxon": "^2.3.1", + "nanoid": "^3.3.4", "node-fetch": "^2.6.7", "node-vibrant": "^3.1.6", "postcss-font-magician": "^3.0.0", diff --git a/package.json b/package.json index d74eb314..def2330e 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "fs-extra": "^10.0.1", "helmet": "^4.6.0", "luxon": "^2.3.1", + "nanoid": "^3.3.4", "node-fetch": "^2.6.7", "node-vibrant": "^3.1.6", "postcss-font-magician": "^3.0.0", diff --git a/src/generators/nanoid.ts b/src/generators/nanoid.ts new file mode 100644 index 00000000..cd87e7e6 --- /dev/null +++ b/src/generators/nanoid.ts @@ -0,0 +1,2 @@ +import { nanoid } from 'nanoid'; +export default ({ length }: { length?: number }) => nanoid(length); From 20b907d676dcd881fc5908bea979029c2e3d8ed7 Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 21:49:58 -0700 Subject: [PATCH 05/59] build: target ES2022 --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 5cec8f8a..6b63df67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,9 @@ "extends": "@tsconfig/node16/tsconfig.json", "compilerOptions": { "outDir": "./dist", - "target": "ES2021", + "target": "ES2022", "lib": [ - "ES2021", + "ES2022", "DOM" ], "allowJs": true, From 7b88ee9638fb8e30789d0433c06e411bc3bd16f7 Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 21:54:50 -0700 Subject: [PATCH 06/59] feat: added type definitions for auth --- src/types/auth.d.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/types/auth.d.ts diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts new file mode 100644 index 00000000..a42e2f62 --- /dev/null +++ b/src/types/auth.d.ts @@ -0,0 +1,64 @@ +/** + * Defines the structure of a user + */ +export interface User { + /** + * Name of the user + */ + username: string + + /** + * Hashed password. Passwords are hashed using bcrypt. + */ + passhash: string + + /** + * Token used for upload authentication + */ + token: string + + /** + * Indicates whether the user is an admin + */ + admin: boolean + + /** + * Extra metadata. Frontends can use this to store extra data. + */ + meta: { + [key: string]: any + } +} + +/** + * Defines the structure of the users.json file + */ +export interface Users { + /** + * List of users. The key is the user's unique ID. + */ + users: { + [key: string]: User + } + + /** + * Indicates whether auth.json has been migrated + */ + migrated?: boolean + + /** + * Extra metadata. Frontends can use this to store extra data. + */ + meta: { + [key: string]: any + } +} + +export interface OldUser { + username: string + count: number +} + +export interface OldUsers { + [key: string]: OldUser +} From 1a9fad707633857cb8f3f28c15d503cc5442e04b Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 21:58:28 -0700 Subject: [PATCH 07/59] feat: BREAKING: overhaul auth file format and handling (etc, expand) BREAKING CHANGE: any hosts with modified deployments of ass utilizing the auth file in its current state will need to fix their modifications. --- src/ass.ts | 8 +- src/auth.ts | 145 ++++++++++++++++++++++++++++++++++++- src/routers/resource.ts | 4 +- src/routers/upload.ts | 26 ++----- src/types/definitions.d.ts | 5 -- src/utils.ts | 5 -- 6 files changed, 156 insertions(+), 37 deletions(-) diff --git a/src/ass.ts b/src/ass.ts index 445e893a..b075ad91 100644 --- a/src/ass.ts +++ b/src/ass.ts @@ -44,7 +44,7 @@ const ROUTERS = { }; // Read users and data -import { users } from './auth'; +import { onStart as AuthOnStart, users } from './auth'; import { data } from './data'; //#endregion @@ -127,10 +127,12 @@ app.use('/:resourceId', (req, _res, next) => (req.resourceId = req.params.resour // Error handler app.use((err: ErrWrap, _req: Request, res: Response) => log.error(err.message).err(err).callback(() => res.sendStatus(CODE_INTERNAL_SERVER_ERROR))); // skipcq: JS-0128 -(function start() { +(async function start() { + await AuthOnStart(); + if (data() == null) setTimeout(start, 100); else log - .info('Users', `${Object.keys(users).length}`) + .info('Users', `${users.size}`) .info('Files', `${data().size}`) .info('Data engine', data().name, data().type) .info('Frontend', ASS_FRONTEND.enabled ? ASS_FRONTEND.brand : 'disabled', `${ASS_FRONTEND.enabled ? `${getTrueHttp()}${getTrueDomain()}${ASS_FRONTEND.endpoint}` : ''}`) diff --git a/src/auth.ts b/src/auth.ts index 304d7bae..607de7da 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,9 +4,152 @@ import fs from 'fs-extra'; import { log, path, arrayEquals } from './utils'; +import { nanoid } from 'nanoid'; +import { User, Users, OldUsers } from './types/auth'; +import { Request } from 'express'; -export const users = require('../auth.json').users || {}; +/** + * !!!!! + * Things for tycrek to do: + * - [ ] Add a way to configure passwords + * - [ ] Create new users + * - [ ] Modify user (admin, meta, replace token/token history) + * - [ ] Delete user + * - [x] Get user + * - [x] Get users + * - [x] Get user by token + */ + +/** + * Map of users + */ +export const users = new Map(); + +/** + * Migrates the old auth.json format to the new one + * @since v0.14.0 + */ +const migrate = (): Promise => new Promise(async (resolve, reject) => { + + // Get ready to read the old auth.json file + const authPath = path('auth.json'); + const oldUsers = fs.readJsonSync(authPath).users as OldUsers; + + // Create a new users object + const newUsers: Users = { users: {}, meta: {} }; + newUsers.migrated = true; + + // Loop through each user + Object.entries(oldUsers).forEach(([token, { username }]) => { + + // Create a new user object + const id = nanoid(); + const newUser: User = { + username: username, + passhash: '', // TODO: Figure out how to configure passwords + token, + admin: Object.keys(oldUsers).indexOf(token) === 0, + meta: {} + }; + + newUsers.users[id] = newUser; + }); + + // Save the new users object to auth.json + fs.writeJson(authPath, newUsers, { spaces: '\t' }) + .then(() => resolve(newUsers)) + .catch(reject); +}); + +/** + * This is a WIP + */ +export const createNewUser = (username: string, passhash: string, admin: boolean, meta?: { [key: string]: User }): Promise => new Promise(async (resolve, reject) => { + + // todo: finish this + + // Create a new user object + const id = nanoid(); + const newUser: User = { + username, + passhash, + token: nanoid(32), + admin, + meta: meta || {} + }; + + // Add the user to the users map + users.set(id, newUser); + + // Save the new user to auth.json + const authPath = path('auth.json'); + const auth = fs.readJsonSync(authPath) as Users; + auth.users[id] = newUser; + fs.writeJson(authPath, auth, { spaces: '\t' }); +}); + + +/** + * Called by ass.ts on startup + */ +export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) => { + const file = path(authFile); + + log.debug('Reading', file); + + // Check if the file exists + fs.stat(file) + + // Create the file if it doesn't exist + .catch((_errStat) => { + log.debug('File does not exist', authFile, 'will be created automatically'); + return fs.writeJson(file, { migrated: true }); + }) + .catch((errWriteJson) => log.error('Failed to create auth.json').callback(reject, errWriteJson)) + + // File exists or was created + .then(() => fs.readJson(file)) + .then((json: Users) => { + + // Check if the file is the old format + if (json.migrated === undefined || !json.migrated) return ( + log.debug('auth.json is in old format, migrating'), + migrate()); + else return json; + }) + .then((json: Users) => { + + // Check if the file is empty + if (Object.keys(json).length === 0) { + log.debug('auth.json is empty, creating default user'); + //return createDefaultUser(); // todo: need to do this + } + + // Add users to the map + Object.entries(json.users).forEach(([uuid, user]) => users.set(uuid, user)); + }) + .catch((errReadJson) => log.error('Failed to read auth.json').callback(reject, errReadJson)) + .then(resolve); +}); + +/** + * Retrieves a user using their upload token. + */ +export const findFromToken = (token: string) => { + for (const [uuid, user] of users) + if (user.token === token) + return { uuid, user }; + return null; +}; + +/** + * Verifies that the upload token in the request exists in the user map + */ +export const verify = (req: Request) => { + return req.headers.authorization && findFromToken(req.headers.authorization); +}; +// todo: This is definitely broken // Monitor auth.json for changes (triggered by running 'npm run new-token') fs.watch(path('auth.json'), { persistent: false }, (eventType: String) => eventType === 'change' && fs.readJson(path('auth.json')) diff --git a/src/routers/resource.ts b/src/routers/resource.ts index 9bcc2d32..068dd873 100644 --- a/src/routers/resource.ts +++ b/src/routers/resource.ts @@ -11,7 +11,7 @@ import { path, log, getTrueHttp, getTrueDomain, formatBytes, formatTimestamp, ge const { diskFilePath, s3enabled, viewDirect, useSia }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_NOT_FOUND, }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); import { data } from '../data'; -import { users } from '../auth'; +import { findFromToken } from '../auth'; import express from 'express'; const router = express.Router(); @@ -49,7 +49,7 @@ router.get('/', (req: Request, res: Response, next) => data().get(req.ass.resour fileIs: fileData.is, title: escape(fileData.originalname), mimetype: fileData.mimetype, - uploader: users[fileData.token].username, + uploader: findFromToken(fileData.token)?.user.username || 'Unknown', timestamp: formatTimestamp(fileData.timestamp, fileData.timeoffset), size: formatBytes(fileData.size), // todo: figure out how to not ignore this diff --git a/src/routers/upload.ts b/src/routers/upload.ts index bcdfa7db..5295b1eb 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -1,4 +1,4 @@ -import { ErrWrap, User } from '../types/definitions'; +import { ErrWrap } from '../types/definitions'; import { Config, MagicNumbers } from 'ass-json'; import fs from 'fs-extra'; @@ -7,9 +7,9 @@ import bb from 'express-busboy'; import { DateTime } from 'luxon'; import { Webhook, MessageBuilder } from 'discord-webhook-node'; import { processUploaded } from '../storage'; -import { path, log, verify, getTrueHttp, getTrueDomain, generateId, formatBytes } from '../utils'; +import { path, log, getTrueHttp, getTrueDomain, generateId, formatBytes } from '../utils'; import { data } from '../data'; -import { users } from '../auth'; +import { findFromToken, verify } from '../auth'; const { maxUploadSize, resourceIdSize, gfyIdSize, resourceIdType, spaceReplace, adminWebhookEnabled, adminWebhookUrl, adminWebhookUsername, adminWebhookAvatar }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_PAYLOAD_TOO_LARGE }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); @@ -35,7 +35,7 @@ bb.extend(router, { router.post('/', (req: Request, res: Response, next: Function) => { req.headers.authorization = req.headers.authorization || ''; req.token = req.headers.authorization.replace(/[^\da-z]/gi, ''); // Strip anything that isn't a digit or ASCII letter - !verify(req, users) ? log.warn('Upload blocked', 'Unauthorized').callback(() => res.sendStatus(CODE_UNAUTHORIZED)) : next(); // skipcq: JS-0093 + !verify(req) ? log.warn('Upload blocked', 'Unauthorized').callback(() => res.sendStatus(CODE_UNAUTHORIZED)) : next(); // skipcq: JS-0093 }); // Upload file @@ -110,7 +110,7 @@ router.post('/', (req: Request, res: Response, next: Function) => { .then(() => { // Log the upload const logInfo = `${req.file!.originalname} (${req.file!.mimetype}, ${formatBytes(req.file.size)})`; - const uploader = users[req.token ?? ''] ? users[req.token ?? ''].username : ''; + const uploader = findFromToken(req.token)?.user.username ?? 'Unknown'; log.success('File uploaded', logInfo, `uploaded by ${uploader}`); // Build the URLs @@ -163,22 +163,6 @@ router.post('/', (req: Request, res: Response, next: Function) => { adminWebhookUsername.trim().length === 0 ? 'ass admin logs' : adminWebhookUsername, adminWebhookAvatar.trim().length === 0 ? ASS_LOGO : adminWebhookAvatar, true); - - // Also update the users upload count - if (!users[req.token ?? '']) { - const generateUsername = () => generateId('random', 20, 0, req.file.size.toString()); // skipcq: JS-0074 - let username: string = generateUsername(); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - while (Object.values(users).findIndex((user: User) => user.username === username) !== -1) // skipcq: JS-0073 - username = generateUsername(); - - users[req.token ?? ''] = { username, count: 0 }; - } - users[req.token ?? ''].count += 1; - fs.writeJsonSync(path('auth.json'), { users }, { spaces: 4 }); - log.debug('Upload request flow completed', ''); }); }) diff --git a/src/types/definitions.d.ts b/src/types/definitions.d.ts index 75c5021c..56288692 100644 --- a/src/types/definitions.d.ts +++ b/src/types/definitions.d.ts @@ -12,11 +12,6 @@ declare global { } } -export interface User { - token: string - username: string -} - export interface FileData { // Data from request file object uuid?: string diff --git a/src/utils.ts b/src/utils.ts index 0748fb11..934f828a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -70,10 +70,6 @@ export function replaceholder(data: string, size: number, timestamp: number, tim export function arrayEquals(arr1: any[], arr2: any[]) { return arr1.length === arr2.length && arr1.slice().sort().every((value: string, index: number) => value === arr2.slice().sort()[index]) -}; - -export function verify(req: Request, users: JSON) { - return req.headers.authorization && Object.prototype.hasOwnProperty.call(users, req.headers.authorization); } const idModes = { @@ -108,7 +104,6 @@ module.exports = { replaceholder, randomHexColour, sanitize, - verify, renameFile: (req: Request, newName: string) => new Promise((resolve: Function, reject) => { try { const paths = [req.file.destination, newName]; From 340b44a3000c64ad9bad56c73cdfa74b59cc9ead Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 21:58:57 -0700 Subject: [PATCH 08/59] fix: use `setURL` instead of `setUrl`, package seems unmaintained --- src/routers/upload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/upload.ts b/src/routers/upload.ts index 5295b1eb..a64a509a 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -130,7 +130,7 @@ router.post('/', (req: Request, res: Response, next: Function) => { const embed = new MessageBuilder() .setTitle(logInfo) // @ts-ignore - .setUrl(resourceUrl) + .setURL(resourceUrl) // I don't know why this is throwing an error when `setUrl` is used but it does. This is a workaround. .setDescription(`${admin ? `**User:** \`${uploader}\`\n` : ''}**Size:** \`${formatBytes(req.file.size)}\`\n**[Delete](${deleteUrl})**`) .setThumbnail(thumbnailUrl) // @ts-ignore From 499aac0f5b20bc27dfb30b9360f96d60eb09149b Mon Sep 17 00:00:00 2001 From: tycrek Date: Tue, 29 Nov 2022 22:00:01 -0700 Subject: [PATCH 09/59] chore: indicate this will become v0.14.0 eventually --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index def2330e..74aca198 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ass", - "version": "0.13.0", + "version": "0.14.0-alpha.1", "description": "The superior self-hosted ShareX server", "main": "ass.js", "engines": { From 27aa264d376451a59b1cd4dafa9af1b33afe6957 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 30 Nov 2022 13:16:44 -0700 Subject: [PATCH 10/59] refactor: switch to arrays instead of Objects/maps --- src/ass.ts | 2 +- src/auth.ts | 27 ++++++++++++--------------- src/routers/resource.ts | 2 +- src/routers/upload.ts | 2 +- src/types/auth.d.ts | 9 ++++++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ass.ts b/src/ass.ts index b075ad91..b5b38d46 100644 --- a/src/ass.ts +++ b/src/ass.ts @@ -132,7 +132,7 @@ app.use((err: ErrWrap, _req: Request, res: Response) => log.error(err.message).e if (data() == null) setTimeout(start, 100); else log - .info('Users', `${users.size}`) + .info('Users', `${users.length}`) .info('Files', `${data().size}`) .info('Data engine', data().name, data().type) .info('Frontend', ASS_FRONTEND.enabled ? ASS_FRONTEND.brand : 'disabled', `${ASS_FRONTEND.enabled ? `${getTrueHttp()}${getTrueDomain()}${ASS_FRONTEND.endpoint}` : ''}`) diff --git a/src/auth.ts b/src/auth.ts index 607de7da..80560e33 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -23,7 +23,7 @@ import { Request } from 'express'; /** * Map of users */ -export const users = new Map(); +export const users = [] as User[]; /** * Migrates the old auth.json format to the new one @@ -36,15 +36,15 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { const oldUsers = fs.readJsonSync(authPath).users as OldUsers; // Create a new users object - const newUsers: Users = { users: {}, meta: {} }; + const newUsers: Users = { users: [], meta: {} }; newUsers.migrated = true; // Loop through each user Object.entries(oldUsers).forEach(([token, { username }]) => { // Create a new user object - const id = nanoid(); const newUser: User = { + unid: nanoid(), username: username, passhash: '', // TODO: Figure out how to configure passwords token, @@ -52,7 +52,7 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { meta: {} }; - newUsers.users[id] = newUser; + newUsers.users.push(newUser); }); // Save the new users object to auth.json @@ -69,8 +69,8 @@ export const createNewUser = (username: string, passhash: string, admin: boolean // todo: finish this // Create a new user object - const id = nanoid(); const newUser: User = { + unid: nanoid(), username, passhash, token: nanoid(32), @@ -79,13 +79,13 @@ export const createNewUser = (username: string, passhash: string, admin: boolean }; // Add the user to the users map - users.set(id, newUser); + users.push(newUser); // Save the new user to auth.json const authPath = path('auth.json'); - const auth = fs.readJsonSync(authPath) as Users; - auth.users[id] = newUser; - fs.writeJson(authPath, auth, { spaces: '\t' }); + const authData = fs.readJsonSync(authPath) as Users; + authData.users.push(newUser); + fs.writeJson(authPath, authData, { spaces: '\t' }); }); @@ -126,20 +126,17 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) } // Add users to the map - Object.entries(json.users).forEach(([uuid, user]) => users.set(uuid, user)); + json.users.forEach((user) => users.push(user)); }) .catch((errReadJson) => log.error('Failed to read auth.json').callback(reject, errReadJson)) .then(resolve); }); /** - * Retrieves a user using their upload token. + * Retrieves a user using their upload token. Returns `null` if the user does not exist. */ export const findFromToken = (token: string) => { - for (const [uuid, user] of users) - if (user.token === token) - return { uuid, user }; - return null; + return users.find((user) => user.token === token) || null; }; /** diff --git a/src/routers/resource.ts b/src/routers/resource.ts index 068dd873..d42c7b0a 100644 --- a/src/routers/resource.ts +++ b/src/routers/resource.ts @@ -49,7 +49,7 @@ router.get('/', (req: Request, res: Response, next) => data().get(req.ass.resour fileIs: fileData.is, title: escape(fileData.originalname), mimetype: fileData.mimetype, - uploader: findFromToken(fileData.token)?.user.username || 'Unknown', + uploader: findFromToken(fileData.token)?.username ?? 'Unknown', timestamp: formatTimestamp(fileData.timestamp, fileData.timeoffset), size: formatBytes(fileData.size), // todo: figure out how to not ignore this diff --git a/src/routers/upload.ts b/src/routers/upload.ts index a64a509a..904d90e7 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -110,7 +110,7 @@ router.post('/', (req: Request, res: Response, next: Function) => { .then(() => { // Log the upload const logInfo = `${req.file!.originalname} (${req.file!.mimetype}, ${formatBytes(req.file.size)})`; - const uploader = findFromToken(req.token)?.user.username ?? 'Unknown'; + const uploader = findFromToken(req.token)?.username ?? 'Unknown'; log.success('File uploaded', logInfo, `uploaded by ${uploader}`); // Build the URLs diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts index a42e2f62..6234ac89 100644 --- a/src/types/auth.d.ts +++ b/src/types/auth.d.ts @@ -2,6 +2,11 @@ * Defines the structure of a user */ export interface User { + /** + * Unique ID, provided by Nano ID + */ + unid: string + /** * Name of the user */ @@ -37,9 +42,7 @@ export interface Users { /** * List of users. The key is the user's unique ID. */ - users: { - [key: string]: User - } + users: User[] /** * Indicates whether auth.json has been migrated From 42a7b4758fa8a6a2c7e6f4d23e385430840176ba Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 30 Nov 2022 14:15:23 -0700 Subject: [PATCH 11/59] chore: rename this to improve understanding --- src/auth.ts | 2 +- src/routers/upload.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 80560e33..10b9d622 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -142,7 +142,7 @@ export const findFromToken = (token: string) => { /** * Verifies that the upload token in the request exists in the user map */ -export const verify = (req: Request) => { +export const verifyValidToken = (req: Request) => { return req.headers.authorization && findFromToken(req.headers.authorization); }; diff --git a/src/routers/upload.ts b/src/routers/upload.ts index 904d90e7..01632d65 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -9,7 +9,7 @@ import { Webhook, MessageBuilder } from 'discord-webhook-node'; import { processUploaded } from '../storage'; import { path, log, getTrueHttp, getTrueDomain, generateId, formatBytes } from '../utils'; import { data } from '../data'; -import { findFromToken, verify } from '../auth'; +import { findFromToken, verifyValidToken } from '../auth'; const { maxUploadSize, resourceIdSize, gfyIdSize, resourceIdType, spaceReplace, adminWebhookEnabled, adminWebhookUrl, adminWebhookUsername, adminWebhookAvatar }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_PAYLOAD_TOO_LARGE }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); @@ -35,7 +35,7 @@ bb.extend(router, { router.post('/', (req: Request, res: Response, next: Function) => { req.headers.authorization = req.headers.authorization || ''; req.token = req.headers.authorization.replace(/[^\da-z]/gi, ''); // Strip anything that isn't a digit or ASCII letter - !verify(req) ? log.warn('Upload blocked', 'Unauthorized').callback(() => res.sendStatus(CODE_UNAUTHORIZED)) : next(); // skipcq: JS-0093 + !verifyValidToken(req) ? log.warn('Upload blocked', 'Unauthorized').callback(() => res.sendStatus(CODE_UNAUTHORIZED)) : next(); // skipcq: JS-0093 }); // Upload file From 6be768d655087482a8a341462274e3df3c88df47 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:23:31 -0700 Subject: [PATCH 12/59] feat: auto-gen random unknown admin password during migration --- src/auth.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 10b9d622..69c3b265 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -7,6 +7,7 @@ import { log, path, arrayEquals } from './utils'; import { nanoid } from 'nanoid'; import { User, Users, OldUsers } from './types/auth'; import { Request } from 'express'; +import bcrypt from 'bcrypt'; /** * !!!!! @@ -40,15 +41,18 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { newUsers.migrated = true; // Loop through each user - Object.entries(oldUsers).forEach(([token, { username }]) => { + Object.entries(oldUsers).forEach(async ([token, { username }]) => { + + // Determine if this user is the admin + const admin = Object.keys(oldUsers).indexOf(token) === 0; // Create a new user object const newUser: User = { unid: nanoid(), username: username, - passhash: '', // TODO: Figure out how to configure passwords + passhash: admin ? await bcrypt.hash(nanoid(32), 10) : '', token, - admin: Object.keys(oldUsers).indexOf(token) === 0, + admin, meta: {} }; From 6cfd353a54a8d0f66fe5271351c3b44deca1287e Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:24:03 -0700 Subject: [PATCH 13/59] feat: added export for setting passwords --- src/auth.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/auth.ts b/src/auth.ts index 69c3b265..7bb8df1d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -92,6 +92,22 @@ export const createNewUser = (username: string, passhash: string, admin: boolean fs.writeJson(authPath, authData, { spaces: '\t' }); }); +export const setUserPassword = (unid: string, password: string): Promise => new Promise(async (resolve, reject) => { + + // Find the user + const user = users.find((user) => user.unid === unid); + if (!user) return reject(new Error('User not found')); + + // Set the password + user.passhash = await bcrypt.hash(password, 10); + + // Save the new user to auth.json + const authPath = path('auth.json'); + const authData = fs.readJsonSync(authPath) as Users; + const userIndex = authData.users.findIndex((user) => user.unid === unid); + authData.users[userIndex] = user; + fs.writeJson(authPath, authData, { spaces: '\t' }); +}); /** * Called by ass.ts on startup From 6338628739cfc4bb071b20027f7247d4b9b4d092 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:24:54 -0700 Subject: [PATCH 14/59] build: forgot to commit packages for bcrypt --- package-lock.json | 641 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 642 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9c5f99d..cc68990d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ass", - "version": "0.13.0", + "version": "0.14.0-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ass", - "version": "0.13.0", + "version": "0.14.0-alpha.1", "license": "ISC", "dependencies": { "@skynetlabs/skynet-nodejs": "^2.3.0", @@ -20,6 +20,7 @@ "any-shell-escape": "^0.1.1", "autoprefixer": "^10.4.4", "aws-sdk": "^2.1115.0", + "bcrypt": "^5.1.0", "check-node-version": "^4.2.1", "crypto-random-string": "3.3.1", "cssnano": "^5.1.7", @@ -47,6 +48,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/bcrypt": "^5.0.0", "@types/escape-html": "^1.0.1", "@types/express": "^4.17.13", "@types/express-brute": "^1.0.1", @@ -760,6 +762,39 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -918,6 +953,15 @@ "url": "https://patreon.com/tycrek" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1138,6 +1182,11 @@ "jdataview": "^2.5.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1247,6 +1296,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1381,6 +1447,11 @@ "node": ">= 10.0.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base32-decode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz", @@ -1418,6 +1489,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1577,6 +1661,15 @@ "node": ">=0.8.0" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -1875,6 +1968,14 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -1916,6 +2017,11 @@ "node": ">= 10" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -1941,6 +2047,11 @@ "node": ">=0.8.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", @@ -2261,6 +2372,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2429,6 +2545,11 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.282.tgz", "integrity": "sha512-Dki0WhHNh/br/Xi1vAkueU5mtIc9XLHcMKB6tNfQKk+kPG0TEUjRh5QEMAUbRp30/rYNMFD1zKKvbVzwq/4wmg==" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -2883,6 +3004,22 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2926,6 +3063,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2993,6 +3189,25 @@ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3118,6 +3333,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/helmet": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", @@ -3195,6 +3415,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3801,6 +4030,20 @@ "node": ">=12" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -3933,6 +4176,17 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", @@ -3941,6 +4195,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4078,6 +4355,20 @@ "querystring": "0.2.0" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4116,6 +4407,17 @@ "node": ">=4" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4338,6 +4640,14 @@ "node": ">=4" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -5362,6 +5672,20 @@ "node": ">= 0.4.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5965,6 +6289,22 @@ "postcss": "^8.0.9" } }, + "node_modules/tar": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", + "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -5991,6 +6331,14 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/timm": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", @@ -6322,6 +6670,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", @@ -7021,6 +7377,32 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7137,6 +7519,15 @@ "fs-extra": "^10.0.0" } }, + "@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -7355,6 +7746,11 @@ "jdataview": "^2.5.0" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7434,6 +7830,20 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -7536,6 +7946,11 @@ "@babel/types": "^7.9.6" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base32-decode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz", @@ -7559,6 +7974,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -7699,6 +8123,15 @@ } } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -7916,6 +8349,11 @@ "simple-swizzle": "^0.2.2" } }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -7948,6 +8386,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -7967,6 +8410,11 @@ "busboy": "~0.3.1" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", @@ -8189,6 +8637,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8319,6 +8772,11 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.282.tgz", "integrity": "sha512-Dki0WhHNh/br/Xi1vAkueU5mtIc9XLHcMKB6tNfQKk+kPG0TEUjRh5QEMAUbRp30/rYNMFD1zKKvbVzwq/4wmg==" }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -8675,6 +9133,19 @@ "universalify": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -8702,6 +9173,52 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8754,6 +9271,19 @@ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8844,6 +9374,11 @@ "has-symbols": "^1.0.2" } }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "helmet": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", @@ -8913,6 +9448,15 @@ } } }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9360,6 +9904,14 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.0.tgz", "integrity": "sha512-IDkEPB80Rb6gCAU+FEib0t4FeJ4uVOuX1CQ9GsvU3O+JAGIgu0J7sf1OarXKaKDygTZIoJyU6YdZzTFRu+YR0A==" }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -9453,11 +10005,36 @@ "dom-walk": "^0.1.0" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -9564,6 +10141,14 @@ } } }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9587,6 +10172,17 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -9752,6 +10348,11 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -10443,6 +11044,14 @@ "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==" }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10862,6 +11471,26 @@ "resolve": "^1.22.1" } }, + "tar": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", + "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + } + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -11139,6 +11768,14 @@ "is-typed-array": "^1.1.9" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "winston": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", diff --git a/package.json b/package.json index 74aca198..5510cdd5 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "any-shell-escape": "^0.1.1", "autoprefixer": "^10.4.4", "aws-sdk": "^2.1115.0", + "bcrypt": "^5.1.0", "check-node-version": "^4.2.1", "crypto-random-string": "3.3.1", "cssnano": "^5.1.7", @@ -77,6 +78,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/bcrypt": "^5.0.0", "@types/escape-html": "^1.0.1", "@types/express": "^4.17.13", "@types/express-brute": "^1.0.1", @@ -93,4 +95,4 @@ "@types/uuid": "^8.3.1", "@types/ws": "^7.4.7" } -} +} \ No newline at end of file From dc2f3937b2dd0b1f36f9d8422c3219cf1b59a8d9 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:25:24 -0700 Subject: [PATCH 15/59] feat: added CLI scripts for setting & testing hashed passwords --- package.json | 4 +++- src/tools/script.setpassword.ts | 19 +++++++++++++++++++ src/tools/script.testpassword.ts | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/tools/script.setpassword.ts create mode 100644 src/tools/script.testpassword.ts diff --git a/package.json b/package.json index 5510cdd5..478c2c68 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "docker-update": "git pull && npm run docker-uplite", "docker-uplite": "docker-compose up --force-recreate --build -d && docker image prune -f", "docker-upfull": "npm run docker-update && npm run docker-resetup", - "docker-resetup": "docker-compose exec ass npm run setup && docker-compose restart" + "docker-resetup": "docker-compose exec ass npm run setup && docker-compose restart", + "cli-setpassword": "node dist/tools/script.setpassword.js", + "cli-testpassword": "node dist/tools/script.testpassword.js" }, "repository": "github:tycrek/ass", "keywords": [ diff --git a/src/tools/script.setpassword.ts b/src/tools/script.setpassword.ts new file mode 100644 index 00000000..ebbd55a7 --- /dev/null +++ b/src/tools/script.setpassword.ts @@ -0,0 +1,19 @@ +import logger from '../logger'; +import { onStart, users, setUserPassword } from '../auth'; + +if (process.argv.length < 4) { + logger.error('Missing username/unid or password'); + process.exit(1); +} else { + const id = process.argv[2]; + const password = process.argv[3]; + + onStart(process.argv[4] || 'auth.json') + .then(() => { + const user = users.find((user) => user.unid === id || user.username === id); + if (!user) throw new Error('User not found'); + else return setUserPassword(user.unid, password); + }) + .then(() => logger.info('Password changed successfully').callback(() => process.exit(0))) + .catch((err) => logger.error(err).callback(() => process.exit(1))); +} diff --git a/src/tools/script.testpassword.ts b/src/tools/script.testpassword.ts new file mode 100644 index 00000000..699ec9a2 --- /dev/null +++ b/src/tools/script.testpassword.ts @@ -0,0 +1,20 @@ +import logger from '../logger'; +import { onStart, users } from '../auth'; +import { compare } from 'bcrypt'; + +if (process.argv.length < 4) { + logger.error('Missing username/unid or password'); + process.exit(1); +} else { + const id = process.argv[2]; + const password = process.argv[3]; + + onStart(process.argv[4] || 'auth.json') + .then(() => { + const user = users.find((user) => user.unid === id || user.username === id); + if (!user) throw new Error('User not found'); + else return compare(password, user.passhash); + }) + .then((result) => logger.info('Matches', `${result}`).callback(() => process.exit(0))) + .catch((err) => logger.error(err).callback(() => process.exit(1))); +} From 2dad6f13cea0bf82c85553af85d7edd6e81b8805 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:25:48 -0700 Subject: [PATCH 16/59] feat: `createNewUser` now accepts a password and handles hashing --- src/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 7bb8df1d..5d5dccc5 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -68,7 +68,7 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { /** * This is a WIP */ -export const createNewUser = (username: string, passhash: string, admin: boolean, meta?: { [key: string]: User }): Promise => new Promise(async (resolve, reject) => { +export const createNewUser = (username: string, password: string, admin: boolean, meta?: { [key: string]: User }): Promise => new Promise(async (resolve, reject) => { // todo: finish this @@ -76,7 +76,7 @@ export const createNewUser = (username: string, passhash: string, admin: boolean const newUser: User = { unid: nanoid(), username, - passhash, + passhash: await bcrypt.hash(password, 10), token: nanoid(32), admin, meta: meta || {} From 1887409eeb10adae2d0071ac2ab17a30968ce24c Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 13:26:56 -0700 Subject: [PATCH 17/59] fix: put salt rounds in a constant --- src/auth.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 5d5dccc5..c660629a 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -9,6 +9,8 @@ import { User, Users, OldUsers } from './types/auth'; import { Request } from 'express'; import bcrypt from 'bcrypt'; +const SALT_ROUNDS = 10; + /** * !!!!! * Things for tycrek to do: @@ -50,7 +52,7 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { const newUser: User = { unid: nanoid(), username: username, - passhash: admin ? await bcrypt.hash(nanoid(32), 10) : '', + passhash: admin ? await bcrypt.hash(nanoid(32), SALT_ROUNDS) : '', token, admin, meta: {} @@ -76,7 +78,7 @@ export const createNewUser = (username: string, password: string, admin: boolean const newUser: User = { unid: nanoid(), username, - passhash: await bcrypt.hash(password, 10), + passhash: await bcrypt.hash(password, SALT_ROUNDS), token: nanoid(32), admin, meta: meta || {} @@ -99,7 +101,7 @@ export const setUserPassword = (unid: string, password: string): Promise = if (!user) return reject(new Error('User not found')); // Set the password - user.passhash = await bcrypt.hash(password, 10); + user.passhash = await bcrypt.hash(password, SALT_ROUNDS); // Save the new user to auth.json const authPath = path('auth.json'); From 619a30d6850f48507e845395111b2740e84e8cc1 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:14:11 -0700 Subject: [PATCH 18/59] fix: auth file written before bcrypt Promise resolved --- src/auth.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index c660629a..4241e2d0 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -43,23 +43,24 @@ const migrate = (): Promise => new Promise(async (resolve, reject) => { newUsers.migrated = true; // Loop through each user - Object.entries(oldUsers).forEach(async ([token, { username }]) => { + await Promise.all(Object.entries(oldUsers).map(async ([token, { username }]) => { // Determine if this user is the admin const admin = Object.keys(oldUsers).indexOf(token) === 0; + const passhash = admin ? await bcrypt.hash(nanoid(32), SALT_ROUNDS) : ''; // Create a new user object const newUser: User = { unid: nanoid(), - username: username, - passhash: admin ? await bcrypt.hash(nanoid(32), SALT_ROUNDS) : '', + username, + passhash, token, admin, meta: {} }; newUsers.users.push(newUser); - }); + })); // Save the new users object to auth.json fs.writeJson(authPath, newUsers, { spaces: '\t' }) From 76dc68405aff212f01b0b251e71d6d130e0c179f Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:29:39 -0700 Subject: [PATCH 19/59] feat: deprecate `token` in FileData, switch to `uploader` --- src/auth.ts | 6 ++++-- src/routers/resource.ts | 4 ++-- src/routers/upload.ts | 2 +- src/types/definitions.d.ts | 6 +++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 4241e2d0..2c83dc88 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,11 +3,13 @@ */ import fs from 'fs-extra'; -import { log, path, arrayEquals } from './utils'; import { nanoid } from 'nanoid'; -import { User, Users, OldUsers } from './types/auth'; import { Request } from 'express'; import bcrypt from 'bcrypt'; +import { log, path, arrayEquals } from './utils'; +import { data } from './data'; +import { User, Users, OldUsers } from './types/auth'; +import { FileData } from './types/definitions'; const SALT_ROUNDS = 10; diff --git a/src/routers/resource.ts b/src/routers/resource.ts index d42c7b0a..aee06d3f 100644 --- a/src/routers/resource.ts +++ b/src/routers/resource.ts @@ -11,7 +11,7 @@ import { path, log, getTrueHttp, getTrueDomain, formatBytes, formatTimestamp, ge const { diskFilePath, s3enabled, viewDirect, useSia }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_NOT_FOUND, }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); import { data } from '../data'; -import { findFromToken } from '../auth'; +import { users } from '../auth'; import express from 'express'; const router = express.Router(); @@ -49,7 +49,7 @@ router.get('/', (req: Request, res: Response, next) => data().get(req.ass.resour fileIs: fileData.is, title: escape(fileData.originalname), mimetype: fileData.mimetype, - uploader: findFromToken(fileData.token)?.username ?? 'Unknown', + uploader: users.find(user => user.unid === fileData.uploader)?.username || 'Unknown', timestamp: formatTimestamp(fileData.timestamp, fileData.timeoffset), size: formatBytes(fileData.size), // todo: figure out how to not ignore this diff --git a/src/routers/upload.ts b/src/routers/upload.ts index 01632d65..be89389e 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -60,7 +60,7 @@ router.post('/', (req: Request, res: Response, next: Function) => { req.file!.timeoffset = req.headers['x-ass-timeoffset']?.toString() || 'UTC+0'; // Keep track of the token that uploaded the resource - req.file.token = req.token ?? ''; + req.file.uploader = findFromToken(req.token)?.unid ?? ''; // Attach any embed overrides, if necessary req.file.opengraph = { diff --git a/src/types/definitions.d.ts b/src/types/definitions.d.ts index 56288692..cbbdfbf4 100644 --- a/src/types/definitions.d.ts +++ b/src/types/definitions.d.ts @@ -38,7 +38,11 @@ export interface FileData { domain: string timestamp: number timeoffset: string - token: string + /** + * @deprecated + */ + token?: string + uploader: string opengraph: OpenGraphData // I found this in utils and idk where it comes from From 6cc0cb3ffae62daf4854e40ad4e6a769d83802a0 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:30:36 -0700 Subject: [PATCH 20/59] feat: use vars for filenames instead of direct strings --- src/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 2c83dc88..e3f73374 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -34,10 +34,10 @@ export const users = [] as User[]; * Migrates the old auth.json format to the new one * @since v0.14.0 */ -const migrate = (): Promise => new Promise(async (resolve, reject) => { +const migrate = (authFileName = 'auth.json'): Promise => new Promise(async (resolve, reject) => { // Get ready to read the old auth.json file - const authPath = path('auth.json'); + const authPath = path(authFileName); const oldUsers = fs.readJsonSync(authPath).users as OldUsers; // Create a new users object @@ -118,8 +118,8 @@ export const setUserPassword = (unid: string, password: string): Promise = * Called by ass.ts on startup */ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) => { - const file = path(authFile); + const file = path(authFile); log.debug('Reading', file); // Check if the file exists @@ -139,7 +139,7 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) // Check if the file is the old format if (json.migrated === undefined || !json.migrated) return ( log.debug('auth.json is in old format, migrating'), - migrate()); + migrate(authFile)); else return json; }) .then((json: Users) => { From 4c4009207ff2d212e93245ee226ca88b193bd82a Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:31:06 -0700 Subject: [PATCH 21/59] feat: migrate datafile (token => uploader unid) --- src/auth.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/auth.ts b/src/auth.ts index e3f73374..a2732dbb 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -66,6 +66,27 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn // Save the new users object to auth.json fs.writeJson(authPath, newUsers, { spaces: '\t' }) + .catch(reject) + + // Migrate the datafile (token => uploader) + .then(() => data().get()) + .then((fileData: [string, FileData][]) => + + // Wait for all the deletions and puts to finish + Promise.all(fileData.map(async ([key, file]) => { + + // We need to use `newUsers` because `users` hasn't been re-assigned yet + const user = newUsers.users.find((user) => user.token === file.token!)?.unid ?? ''; // ? This is probably fine + + // Because of the stupid way I wrote papito, we need to DEL before we can PUT + await data().del(key); + + // PUT the new data + return data().put(key, { ...file, uploader: user }); + }))) + + // We did it hoofuckingray + .then(() => log.success('Migrated all auth & file data to new auth system')) .then(() => resolve(newUsers)) .catch(reject); }); From a774ab311b15fc2cac6aa9c43cd70ae474d29ca4 Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:31:28 -0700 Subject: [PATCH 22/59] feat: try to reset user array at start of onStart* I don't think it works --- src/auth.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/auth.ts b/src/auth.ts index a2732dbb..85b310f2 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -139,6 +139,9 @@ export const setUserPassword = (unid: string, password: string): Promise = * Called by ass.ts on startup */ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) => { + // Reset user array (https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript#1232046) + // ! I don't think this works properly..? + users.splice(0, users.length); const file = path(authFile); log.debug('Reading', file); From 87c8e64698cb046df0cee7f75567a5afb50d98af Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:40:18 -0700 Subject: [PATCH 23/59] fix: remove this method for duplicating the list (ironically correctly) --- src/auth.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 85b310f2..0afac37b 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -195,9 +195,9 @@ export const verifyValidToken = (req: Request) => { return req.headers.authorization && findFromToken(req.headers.authorization); }; -// todo: This is definitely broken +// todo: move inside of onStart (currently broken) // Monitor auth.json for changes (triggered by running 'npm run new-token') -fs.watch(path('auth.json'), { persistent: false }, +/* fs.watch(path('auth.json'), { persistent: false }, (eventType: String) => eventType === 'change' && fs.readJson(path('auth.json')) .then((json: { users: JSON[] }) => { if (!(arrayEquals(Object.keys(users), Object.keys(json.users)))) { @@ -207,3 +207,4 @@ fs.watch(path('auth.json'), { persistent: false }, } }) .catch(console.error)); + */ \ No newline at end of file From 0b5b7d703875ed74dac7854809306d7e280985cc Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 14:43:01 -0700 Subject: [PATCH 24/59] chore: hide backup files --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d339572c..57c63b16 100755 --- a/.gitignore +++ b/.gitignore @@ -104,11 +104,11 @@ dist .tern-port # tokens -auth.json +auth.json* auth.*.json # data -data.json +data.json* # uploads uploads/ From fd06e921915794f02df3d9000b327c9a0236142f Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 16:03:26 -0700 Subject: [PATCH 25/59] fix: API auth middleware for new auth system --- src/routers/api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index c0281302..f99f06c8 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -5,7 +5,7 @@ */ import { Router, Request, Response, NextFunction } from 'express'; -import { users } from '../auth'; +import { findFromToken, users } from '../auth'; import { data } from '../data'; const RouterApi = Router(); @@ -16,8 +16,8 @@ const RouterResource = Router(); * Token authentication middleware */ const authMiddleware = (req: Request, res: Response, next: NextFunction) => { - const token = req.headers.authorization; - (token && users[token]) + const user = findFromToken(req.headers.authorization ?? ''); + (user && user.admin) ? next() : res.sendStatus(401); }; From 6c9b8166e95b9969ea5c039d4dd40d04083fafdf Mon Sep 17 00:00:00 2001 From: tycrek Date: Wed, 7 Dec 2022 16:05:18 -0700 Subject: [PATCH 26/59] feat: use functions instead because why not --- src/routers/api.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index f99f06c8..5fdc92d0 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -9,8 +9,17 @@ import { findFromToken, users } from '../auth'; import { data } from '../data'; const RouterApi = Router(); -const RouterUser = Router(); -const RouterResource = Router(); + +function buildUserRouter() { + const RouterUser = Router(); + + return RouterUser; +} +function buildResourceRouter() { + const RouterResource = Router(); + + return RouterResource; +} /** * Token authentication middleware @@ -23,8 +32,8 @@ const authMiddleware = (req: Request, res: Response, next: NextFunction) => { }; export const onStart = () => { - RouterApi.use('/user', RouterUser); - RouterApi.use('/resource', RouterResource); + RouterApi.use('/user', buildUserRouter()); + RouterApi.use('/resource', buildResourceRouter()); return RouterApi; }; From ac8c861bb100805b7850e680ef56e3a54fec4e08 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:51:37 -0700 Subject: [PATCH 27/59] feat: add middleware function for verifying admin users --- src/routers/api.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/routers/api.ts b/src/routers/api.ts index 5fdc92d0..19159a25 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -7,9 +7,20 @@ import { Router, Request, Response, NextFunction } from 'express'; import { findFromToken, users } from '../auth'; import { data } from '../data'; +import { User } from '../types/auth'; +/** + * The primary API router + */ const RouterApi = Router(); +/** + * Token authentication middleware for Admins + */ +const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => { + const user = findFromToken(req.headers.authorization ?? ''); + (user && user.admin) ? next() : res.sendStatus(401); +}; function buildUserRouter() { const RouterUser = Router(); From 118039f84997b7eccb7c0ea62f00f12f764d9ca8 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:52:29 -0700 Subject: [PATCH 28/59] fix: user names that make sense --- src/routers/api.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index 19159a25..b4fd4c58 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -22,14 +22,15 @@ const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => (user && user.admin) ? next() : res.sendStatus(401); }; function buildUserRouter() { - const RouterUser = Router(); + const userRouter = Router(); - return RouterUser; + return userRouter; } + function buildResourceRouter() { - const RouterResource = Router(); + const resourceRouter = Router(); - return RouterResource; + return resourceRouter; } /** From 55401441fa644727933166d879c74f5e6ca5fa8e Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:52:47 -0700 Subject: [PATCH 29/59] fix: remove duplicate function --- src/routers/api.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index b4fd4c58..246cce68 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -33,16 +33,6 @@ function buildResourceRouter() { return resourceRouter; } -/** - * Token authentication middleware - */ -const authMiddleware = (req: Request, res: Response, next: NextFunction) => { - const user = findFromToken(req.headers.authorization ?? ''); - (user && user.admin) - ? next() - : res.sendStatus(401); -}; - export const onStart = () => { RouterApi.use('/user', buildUserRouter()); RouterApi.use('/resource', buildResourceRouter()); From cb1d75ff1d15a66bfe7fa9692613560236eb0847 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:53:39 -0700 Subject: [PATCH 30/59] feat: added user API routes --- src/routers/api.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/routers/api.ts b/src/routers/api.ts index 246cce68..ce086a25 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -21,9 +21,35 @@ const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => const user = findFromToken(req.headers.authorization ?? ''); (user && user.admin) ? next() : res.sendStatus(401); }; + +/** + * Simple function to either return JSON or a 404, so I don't have to write it 40 times. + */ +const userFinder = (res: Response, user: User | undefined) => user ? res.json(user) : res.sendStatus(404); + function buildUserRouter() { const userRouter = Router(); + // Index + userRouter.get('/', (_req: Request, res: Response) => res.sendStatus(200)); + + // Get all users + // Admin only + userRouter.get('/all', adminAuthMiddleware, (req: Request, res: Response) => res.json(users)); + + // Get self + userRouter.get('/self', (req: Request, res: Response) => + userFinder(res, findFromToken(req.headers['authorization'] ?? '') ?? undefined)); + + // Get user by token + userRouter.get('/token/:token', (req: Request, res: Response) => + userFinder(res, users.find(user => user.token === req.params.token))); + + // Get a user (must be last as it's a catch-all) + // Admin only + userRouter.get('/:id', adminAuthMiddleware, (req: Request, res: Response) => + userFinder(res, users.find(user => user.unid === req.params.id || user.username === req.params.id))); + return userRouter; } From 16ba3ca438ab26789be4d6dfec3ed2d9168bd2d9 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:54:46 -0700 Subject: [PATCH 31/59] feat: allow password resets over the API --- src/ass.ts | 6 +++++- src/routers/api.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ass.ts b/src/ass.ts index b5b38d46..097812f6 100644 --- a/src/ass.ts +++ b/src/ass.ts @@ -3,7 +3,7 @@ import { Config, MagicNumbers, Package } from 'ass-json'; //#region Imports import fs from 'fs-extra'; -import express, { Request, Response } from 'express'; +import express, { Request, Response, json as BodyParserJson } from 'express'; import nofavicon from '@tycrek/express-nofavicon'; import { epcss } from '@tycrek/express-postcss'; import tailwindcss from 'tailwindcss'; @@ -80,6 +80,10 @@ app.get(['/'], bruteforce.prevent, (_req, _res, next) => next()); // Express logger middleware app.use(log.middleware()); +// Body parser for API POST requests +// (I really don't like this being top level but it does not work inside the API Router as of 2022-12-24) +app.use(BodyParserJson()); + // Helmet security middleware app.use(helmet.noSniff()); app.use(helmet.ieNoOpen()); diff --git a/src/routers/api.ts b/src/routers/api.ts index ce086a25..a7a0675a 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -5,7 +5,7 @@ */ import { Router, Request, Response, NextFunction } from 'express'; -import { findFromToken, users } from '../auth'; +import { findFromToken, setUserPassword, users } from '../auth'; import { data } from '../data'; import { User } from '../types/auth'; @@ -45,6 +45,17 @@ function buildUserRouter() { userRouter.get('/token/:token', (req: Request, res: Response) => userFinder(res, users.find(user => user.token === req.params.token))); + // Reset password (new plaintext password in form data; HOST SHOULD BE USING HTTPS) + // Admin only + userRouter.post('/reset', adminAuthMiddleware, (req: Request, res: Response) => { + const id = req.body.id; + const newPassword = req.body.password; + + setUserPassword(id, newPassword) + .then(() => res.sendStatus(200)) + .catch(() => res.sendStatus(500)); + }); + // Get a user (must be last as it's a catch-all) // Admin only userRouter.get('/:id', adminAuthMiddleware, (req: Request, res: Response) => From 0af13589ff56d54a4053a729583b408d951b2bb8 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:55:03 -0700 Subject: [PATCH 32/59] docs: add new user system to README --- .github/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/README.md b/.github/README.md index e179b447..a75a75d3 100644 --- a/.github/README.md +++ b/.github/README.md @@ -337,6 +337,17 @@ S3 servers are generally very fast & have very good uptime, though this will dep [Amazon S3]: https://en.wikipedia.org/wiki/Amazon_S3 [Skynet Labs]: https://github.com/SkynetLabs +## New user system (v0.14.0) + +The user system was overhauled in v0.14.0 to allow more features and flexibility. New fields on users include `admin`, `passhash`, `unid`, and `meta` (these will be documented more once the system is finalized). + +ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. Adding new users via `npm run new-token ` should work as expected, though you'll need to re-launch ass to load the new file. + +**Things still borked:** + +- Creating a default user on new installs +- Creating/modifying/deleting users via the API +- The filewatcher that reloads `auth.json` when modified on CLI (to be changed in the future) ## Custom frontends - OUTDATED **Please be aware that this section is outdated (marked as of 2022-04-15). It will be updated when I overhaul the frontend system.** From ad3b7435b6a5ff2c36dbca37a74d86e13db7bac7 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:55:20 -0700 Subject: [PATCH 33/59] docs: add API to README --- .github/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/README.md b/.github/README.md index a75a75d3..76610012 100644 --- a/.github/README.md +++ b/.github/README.md @@ -348,6 +348,29 @@ ass will automatically convert your old `auth.json` to the new format. **Always - Creating a default user on new installs - Creating/modifying/deleting users via the API - The filewatcher that reloads `auth.json` when modified on CLI (to be changed in the future) + +## Developer API + +ass includes an API (v0.14.0) for frontend developers to easily integrate with. Right now the API is pretty limited but I will expand on it in the future, with frontend developer feedback. + +Any endpoints requiring authorization will require an `Authorization` header with the value being the user's upload token. Admin users are a new feature introduced in v0.14.0. Admin users can access all endpoints, while non-admin users can only access those relevant to them. + +Other things to note: + +- **All endpoints are prefixed with `/api/`**. +- All endpoints will return a JSON object unless otherwise specified. +- Successful endpoints *should* return a `200` status code. Any errors will use the corresponding `4xx` or `5xx` status code (such as `401 Unauthorized`). + +### API endpoints + +| Endpoint | Purpose | Admin? | +| -------- | ------- | ------ | +| **`GET /user/all`** | Returns a list of all users | Yes | +| **`GET /user/self`** | Returns the current user | No | +| **`GET /user/token/:token`** | Returns the user with the given token | No | +| **`POST /user/reset`** | Resets the current user's **password** (token resets coming soon) | No | +| **`GET /user/:id`** | Returns the user with the given ID | Yes | + ## Custom frontends - OUTDATED **Please be aware that this section is outdated (marked as of 2022-04-15). It will be updated when I overhaul the frontend system.** From 45f77001a0e76f88f76a7232a9bbfa2a81a5cb00 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 16:55:27 -0700 Subject: [PATCH 34/59] docs: small note --- .github/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/README.md b/.github/README.md index 76610012..5acbfcc8 100644 --- a/.github/README.md +++ b/.github/README.md @@ -375,6 +375,8 @@ Other things to note: **Please be aware that this section is outdated (marked as of 2022-04-15). It will be updated when I overhaul the frontend system.** +**Update 2022-12-24: I plan to overhaul this early in 2023.** + ass is intended to provide a strong backend for developers to build their own frontends around. [Git Submodules] make it easy to create custom frontends. Submodules are their own projects, which means you are free to build the router however you wish, as long as it exports the required items. A custom frontend is really just an [Express.js router]. **For a detailed walkthrough on developing your first frontend, [consult the wiki][ctw1].** From 578d33163c5bd4b3493149ca6f9a1aa97faed209 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 17:21:01 -0700 Subject: [PATCH 35/59] build: add `dev-win` script for skipping NODE_OPTIONS on Windows --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 478c2c68..031b68e3 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "dev": "npm run build && npm start", + "dev-win": "npm run build-skip-options && npm run start", "build": "NODE_OPTIONS=\"--max-old-space-size=1024\" tsc", "build-skip-options": "tsc", "start": "node dist/ass.js", From 338f6a73e1078e180a12bcb69f67753c1e1aaec2 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 18:54:35 -0700 Subject: [PATCH 36/59] feat: switch to `@tycrek/discord-hookr` for webhooks --- package-lock.json | 209 ++++++++++++++++++++++++++++++++---------- package.json | 2 +- src/routers/upload.ts | 13 ++- 3 files changed, 166 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc68990d..f9a8d35b 100755 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@skynetlabs/skynet-nodejs": "^2.3.0", "@tsconfig/node16": "^1.0.1", + "@tycrek/discord-hookr": "^0.1.0", "@tycrek/express-nofavicon": "^1.0.3", "@tycrek/express-postcss": "^0.2.4", "@tycrek/isprod": "^2.0.2", @@ -24,7 +25,6 @@ "check-node-version": "^4.2.1", "crypto-random-string": "3.3.1", "cssnano": "^5.1.7", - "discord-webhook-node": "^1.1.8", "escape-html": "^1.0.3", "express": "^4.17.3", "express-brute": "^1.0.1", @@ -867,6 +867,33 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" }, + "node_modules/@tycrek/discord-hookr": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@tycrek/discord-hookr/-/discord-hookr-0.1.0.tgz", + "integrity": "sha512-Xlo99oeRBgwtV7YmW3ZjorGX5x6gxSUt/x7PUX8EMejWeWsE/YwyEqIO1g29wRiz3C2c0mZt53hWELesZVg9Aw==", + "dependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.11.17", + "axios": "^1.2.1", + "formdata-polyfill": "^4.0.10", + "typescript": "^4.9.4" + } + }, + "node_modules/@tycrek/discord-hookr/node_modules/@types/node": { + "version": "18.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", + "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" + }, + "node_modules/@tycrek/discord-hookr/node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@tycrek/express-nofavicon": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tycrek/express-nofavicon/-/express-nofavicon-1.0.3.tgz", @@ -2447,28 +2474,6 @@ "is-woff2": "^1.0.0" } }, - "node_modules/discord-webhook-node": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/discord-webhook-node/-/discord-webhook-node-1.1.8.tgz", - "integrity": "sha512-3u0rrwywwYGc6HrgYirN/9gkBYqmdpvReyQjapoXARAHi0P0fIyf3W5tS5i3U3cc7e44E+e7dIHYUeec7yWaug==", - "dependencies": { - "form-data": "^3.0.0", - "node-fetch": "^2.6.0" - } - }, - "node_modules/discord-webhook-node/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2843,6 +2848,28 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/ffmpeg-static": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-4.4.1.tgz", @@ -2958,6 +2985,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4303,6 +4341,24 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5321,6 +5377,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pug": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", @@ -6449,9 +6510,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6606,6 +6667,14 @@ "node": ">=0.10.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -7463,6 +7532,35 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" }, + "@tycrek/discord-hookr": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@tycrek/discord-hookr/-/discord-hookr-0.1.0.tgz", + "integrity": "sha512-Xlo99oeRBgwtV7YmW3ZjorGX5x6gxSUt/x7PUX8EMejWeWsE/YwyEqIO1g29wRiz3C2c0mZt53hWELesZVg9Aw==", + "requires": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.11.17", + "axios": "^1.2.1", + "formdata-polyfill": "^4.0.10", + "typescript": "^4.9.4" + }, + "dependencies": { + "@types/node": { + "version": "18.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", + "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" + }, + "axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } + } + }, "@tycrek/express-nofavicon": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tycrek/express-nofavicon/-/express-nofavicon-1.0.3.tgz", @@ -8693,27 +8791,6 @@ "is-woff2": "^1.0.0" } }, - "discord-webhook-node": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/discord-webhook-node/-/discord-webhook-node-1.1.8.tgz", - "integrity": "sha512-3u0rrwywwYGc6HrgYirN/9gkBYqmdpvReyQjapoXARAHi0P0fIyf3W5tS5i3U3cc7e44E+e7dIHYUeec7yWaug==", - "requires": { - "form-data": "^3.0.0", - "node-fetch": "^2.6.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -9019,6 +9096,15 @@ "reusify": "^1.0.4" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "ffmpeg-static": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-4.4.1.tgz", @@ -9103,6 +9189,14 @@ "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10098,6 +10192,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -10766,6 +10865,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pug": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", @@ -11603,9 +11707,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==" }, "unbox-primitive": { "version": "1.0.2", @@ -11716,6 +11820,11 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 031b68e3..ddfdca47 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "@skynetlabs/skynet-nodejs": "^2.3.0", "@tsconfig/node16": "^1.0.1", + "@tycrek/discord-hookr": "^0.1.0", "@tycrek/express-nofavicon": "^1.0.3", "@tycrek/express-postcss": "^0.2.4", "@tycrek/isprod": "^2.0.2", @@ -57,7 +58,6 @@ "check-node-version": "^4.2.1", "crypto-random-string": "3.3.1", "cssnano": "^5.1.7", - "discord-webhook-node": "^1.1.8", "escape-html": "^1.0.3", "express": "^4.17.3", "express-brute": "^1.0.1", diff --git a/src/routers/upload.ts b/src/routers/upload.ts index be89389e..5e81590b 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -5,7 +5,8 @@ import fs from 'fs-extra'; import bb from 'express-busboy'; //const rateLimit = require('express-rate-limit'); import { DateTime } from 'luxon'; -import { Webhook, MessageBuilder } from 'discord-webhook-node'; +import { Webhook, EmbedBuilder } from '@tycrek/discord-hookr'; + import { processUploaded } from '../storage'; import { path, log, getTrueHttp, getTrueDomain, generateId, formatBytes } from '../utils'; import { data } from '../data'; @@ -127,19 +128,17 @@ router.post('/', (req: Request, res: Response, next: Function) => { hook.setAvatar(avatar); // Build the embed - const embed = new MessageBuilder() + const embed = new EmbedBuilder() .setTitle(logInfo) - // @ts-ignore - .setURL(resourceUrl) // I don't know why this is throwing an error when `setUrl` is used but it does. This is a workaround. + .setURL(resourceUrl) .setDescription(`${admin ? `**User:** \`${uploader}\`\n` : ''}**Size:** \`${formatBytes(req.file.size)}\`\n**[Delete](${deleteUrl})**`) - .setThumbnail(thumbnailUrl) - // @ts-ignore + .setThumbnail({ url: thumbnailUrl }) .setColor(req.file.vibrant) .setTimestamp(); // Send the embed to the webhook, then delete the client after to free resources log.debug(`Sending${admin ? ' admin' : ''} embed to webhook`); - hook.send(embed) + hook.addEmbed(embed).send() .then(() => log.debug(`Webhook${admin ? ' admin' : ''} sent`)) .catch((err) => log.error('Webhook error').err(err)); } From 20db648bc1592f903cf230bec9b8ca006b76a422 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 18:55:00 -0700 Subject: [PATCH 37/59] feat: display ass version in Webhook author --- src/routers/upload.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routers/upload.ts b/src/routers/upload.ts index 5e81590b..e1f2f4b1 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -1,5 +1,5 @@ import { ErrWrap } from '../types/definitions'; -import { Config, MagicNumbers } from 'ass-json'; +import { Config, MagicNumbers, Package } from 'ass-json'; import fs from 'fs-extra'; import bb from 'express-busboy'; @@ -11,8 +11,10 @@ import { processUploaded } from '../storage'; import { path, log, getTrueHttp, getTrueDomain, generateId, formatBytes } from '../utils'; import { data } from '../data'; import { findFromToken, verifyValidToken } from '../auth'; + const { maxUploadSize, resourceIdSize, gfyIdSize, resourceIdType, spaceReplace, adminWebhookEnabled, adminWebhookUrl, adminWebhookUsername, adminWebhookAvatar }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_PAYLOAD_TOO_LARGE }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); +const { name, version, homepage }: Package = fs.readJsonSync(path('package.json')); const ASS_LOGO = 'https://cdn.discordapp.com/icons/848274994375294986/8d339d4a2f3f54b2295e5e0ff62bd9e6.png?size=1024'; import express, { Request, Response } from 'express'; @@ -131,6 +133,7 @@ router.post('/', (req: Request, res: Response, next: Function) => { const embed = new EmbedBuilder() .setTitle(logInfo) .setURL(resourceUrl) + .setAuthor({ name: `${name} ${version}`, url: homepage, icon_url: ASS_LOGO }) .setDescription(`${admin ? `**User:** \`${uploader}\`\n` : ''}**Size:** \`${formatBytes(req.file.size)}\`\n**[Delete](${deleteUrl})**`) .setThumbnail({ url: thumbnailUrl }) .setColor(req.file.vibrant) From 093f84b6fb785ac1a2fa4ab7c1a9c3ecd237eb95 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 19:12:14 -0700 Subject: [PATCH 38/59] fix: added missing link variable to Tailwind theme --- tailwind.config.js | 1 + tailwind.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index d3d380dc..eea89bd0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -53,6 +53,7 @@ module.exports = { colors: { 'primary': theme.txtPrimary || defaults.txtPrimary, 'secondary': theme.txtSecondary || defaults.txtSecondary, + 'link-primary': theme.linkPrimary || defaults.linkPrimary, 'link-hover': theme.linkHover || defaults.linkHover, 'link-active': theme.linkActive || defaults.linkActive, }, diff --git a/tailwind.css b/tailwind.css index 319e20e9..4a529832 100644 --- a/tailwind.css +++ b/tailwind.css @@ -13,7 +13,7 @@ @apply no-underline hover_no-underline active_no-underline visited_no-underline /* regular, visited */ - text-link visited_text-link + text-link-primary visited_text-link-primary border-b-2 visited_border-b-2 border-transparent visited_border-transparent rounded-sm visited_rounded-sm From 77870d122828204e13a9e70132cea24b80fccb63 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 19:12:25 -0700 Subject: [PATCH 39/59] style: make `txtSecondary` darker --- tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index eea89bd0..81e6b341 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,7 +19,7 @@ const defaults = { // Text colours txtPrimary: '#BDBDBD', - txtSecondary: '#BDBDBD', + txtSecondary: '#8D8D8D', // Links linkPrimary: '#FD842D', From 73631d01b4b1d85f4eedf151d355a605c4270aca Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 19:30:15 -0700 Subject: [PATCH 40/59] fix: image was full-width on web but stretched (closes # #178) Thanks to @davidnjoy for alerting of this --- views/view.pug | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/views/view.pug b/views/view.pug index c6aefa50..0421cc1f 100644 --- a/views/view.pug +++ b/views/view.pug @@ -25,15 +25,16 @@ html .w-full.h-full.flex.justify-center.items-center.text-center .bg-viewer.rounded-24 h4.mx-4.mt-6.mb-4.text-3xl.font-main!=title - figure.block.mx-10.my-4.flex.flex-col.align-items-center - if fileIs.video - video.res-media(controls loop muted playsinline preload='metadata')&attributes(resourceAttr) - else if fileIs.image - img.res-media(decoding='async')&attributes(resourceAttr) - else if fileIs.audio - audio.res-media(controls loop preload='metadata')&attributes(resourceAttr) - else - code!=mimetype + figure.mx-10.my-4.flex.flex-col.align-items-center.justify-center + .flex.justify-center + if fileIs.video + video.res-media(controls loop muted playsinline preload='metadata')&attributes(resourceAttr) + else if fileIs.image + img.res-media(decoding='async')&attributes(resourceAttr) + else if fileIs.audio + audio.res-media(controls loop preload='metadata')&attributes(resourceAttr) + else + code!=mimetype figcaption br span.text-2xl Uploaded by #[strong!=uploader] From fe79d8466246ae2323bd1abe338adf63b06dd2df Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 20:02:46 -0700 Subject: [PATCH 41/59] feat: allow using the generated ID in the web viewer (closes #181) --- src/routers/resource.ts | 4 ++-- src/setup.js | 14 ++++++++++++++ src/types/json.d.ts | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/routers/resource.ts b/src/routers/resource.ts index aee06d3f..82b0b7c0 100644 --- a/src/routers/resource.ts +++ b/src/routers/resource.ts @@ -8,7 +8,7 @@ import { Request, Response } from 'express'; import { deleteS3 } from '../storage'; import { SkynetDelete, SkynetDownload } from '../skynet'; import { path, log, getTrueHttp, getTrueDomain, formatBytes, formatTimestamp, getS3url, getDirectUrl, getResourceColor, replaceholder } from '../utils'; -const { diskFilePath, s3enabled, viewDirect, useSia }: Config = fs.readJsonSync(path('config.json')); +const { diskFilePath, s3enabled, viewDirect, useIdInViewer, idInViewerExtension, useSia }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_NOT_FOUND, }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); import { data } from '../data'; import { users } from '../auth'; @@ -47,7 +47,7 @@ router.get('/', (req: Request, res: Response, next) => data().get(req.ass.resour // Send the view to the client res.render('view', { fileIs: fileData.is, - title: escape(fileData.originalname), + title: useIdInViewer ? `${resourceId}${idInViewerExtension ? `${fileData.ext}` : ''}` : escape(fileData.originalname), mimetype: fileData.mimetype, uploader: users.find(user => user.unid === fileData.uploader)?.username || 'Unknown', timestamp: formatTimestamp(fileData.timestamp, fileData.timeoffset), diff --git a/src/setup.js b/src/setup.js index 4a1a9641..b1a2ce76 100644 --- a/src/setup.js +++ b/src/setup.js @@ -12,6 +12,8 @@ const config = { spaceReplace: '_', mediaStrict: false, viewDirect: false, + useIdInViewer: false, + idInViewerExtension: false, dataEngine: '@tycrek/papito', frontendName: 'ass-x', useSia: false, @@ -168,6 +170,18 @@ function doSetup() { default: config.viewDirect, required: false }, + useIdInViewer: { + description: 'Use the ID in the web viewer instead of the filename', + type: 'boolean', + default: config.useIdInViewer, + required: false + }, + idInViewerExtension: { + description: '(Only applies if "useIdInViewer" is true) Include the file extension in the ID in the web viewer', + type: 'boolean', + default: config.idInViewerExtension, + required: false + }, dataEngine: { description: 'Data engine to use (must match an npm package name. If unsure, leave blank)', type: 'string', diff --git a/src/types/json.d.ts b/src/types/json.d.ts index 7d0fd5ea..5525320f 100644 --- a/src/types/json.d.ts +++ b/src/types/json.d.ts @@ -12,6 +12,8 @@ declare module 'ass-json' { gfyIdSize: number mediaStrict: boolean viewDirect: boolean + useIdInViewer: boolean + idInViewerExtension: boolean dataEngine: string frontendName: string indexFile: string From 6dc539c7d58fe4c8d9428939b583dd9299f3780a Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 20:08:36 -0700 Subject: [PATCH 42/59] fix: show original if user overrode with zws --- src/generators/zws.ts | 1 + src/routers/resource.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/generators/zws.ts b/src/generators/zws.ts index 9f54cbe1..4d91d231 100644 --- a/src/generators/zws.ts +++ b/src/generators/zws.ts @@ -1,3 +1,4 @@ import lengthGen from './lengthGen'; const zeroWidthChars = ['\u200B', '\u200C', '\u200D', '\u2060']; export default ({ length }: { length: number }) => lengthGen(length, zeroWidthChars); +export const checkIfZws = (str: string) => str.split('').every(char => zeroWidthChars.includes(char)); diff --git a/src/routers/resource.ts b/src/routers/resource.ts index 82b0b7c0..b8189722 100644 --- a/src/routers/resource.ts +++ b/src/routers/resource.ts @@ -7,6 +7,7 @@ import fetch, { Response as FetchResponse } from 'node-fetch'; import { Request, Response } from 'express'; import { deleteS3 } from '../storage'; import { SkynetDelete, SkynetDownload } from '../skynet'; +import { checkIfZws } from '../generators/zws'; import { path, log, getTrueHttp, getTrueDomain, formatBytes, formatTimestamp, getS3url, getDirectUrl, getResourceColor, replaceholder } from '../utils'; const { diskFilePath, s3enabled, viewDirect, useIdInViewer, idInViewerExtension, useSia }: Config = fs.readJsonSync(path('config.json')); const { CODE_UNAUTHORIZED, CODE_NOT_FOUND, }: MagicNumbers = fs.readJsonSync(path('MagicNumbers.json')); @@ -47,7 +48,7 @@ router.get('/', (req: Request, res: Response, next) => data().get(req.ass.resour // Send the view to the client res.render('view', { fileIs: fileData.is, - title: useIdInViewer ? `${resourceId}${idInViewerExtension ? `${fileData.ext}` : ''}` : escape(fileData.originalname), + title: useIdInViewer && !checkIfZws(resourceId) ? `${resourceId}${idInViewerExtension ? `${fileData.ext}` : ''}` : escape(fileData.originalname), mimetype: fileData.mimetype, uploader: users.find(user => user.unid === fileData.uploader)?.username || 'Unknown', timestamp: formatTimestamp(fileData.timestamp, fileData.timeoffset), From 717b788e83c6f5c5fc69d45f167410137d8fe2b3 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 20:40:33 -0700 Subject: [PATCH 43/59] feat: added server-side embed configuration (closes #173) --- src/routers/upload.ts | 21 +++++++++++++-------- src/types/json.d.ts | 10 ++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/routers/upload.ts b/src/routers/upload.ts index e1f2f4b1..0a5cddbf 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -1,5 +1,5 @@ import { ErrWrap } from '../types/definitions'; -import { Config, MagicNumbers, Package } from 'ass-json'; +import { Config, MagicNumbers, Package, ServerSideEmbed } from 'ass-json'; import fs from 'fs-extra'; import bb from 'express-busboy'; @@ -65,15 +65,20 @@ router.post('/', (req: Request, res: Response, next: Function) => { // Keep track of the token that uploaded the resource req.file.uploader = findFromToken(req.token)?.unid ?? ''; + // Load server-side embed config, if it exists + const ssePath = path('share/embed.json'); + const sse: ServerSideEmbed | undefined = fs.existsSync(ssePath) ? fs.readJsonSync(path('share/embed.json')) : undefined; + const useSse = sse && sse.title != undefined && sse.title != ''; + // Attach any embed overrides, if necessary req.file.opengraph = { - title: req.headers['x-ass-og-title'], - description: req.headers['x-ass-og-description'], - author: req.headers['x-ass-og-author'], - authorUrl: req.headers['x-ass-og-author-url'], - provider: req.headers['x-ass-og-provider'], - providerUrl: req.headers['x-ass-og-provider-url'], - color: req.headers['x-ass-og-color'] + title: useSse ? sse.title : req.headers['x-ass-og-title'], + description: useSse ? sse.description : req.headers['x-ass-og-description'], + author: useSse ? sse.author : req.headers['x-ass-og-author'], + authorUrl: useSse ? sse.authorUrl : req.headers['x-ass-og-author-url'], + provider: useSse ? sse.provider : req.headers['x-ass-og-provider'], + providerUrl: useSse ? sse.providerUrl : req.headers['x-ass-og-provider-url'], + color: useSse ? sse.color : req.headers['x-ass-og-color'] }; // Fix spaces in originalname diff --git a/src/types/json.d.ts b/src/types/json.d.ts index 5525320f..da3fba65 100644 --- a/src/types/json.d.ts +++ b/src/types/json.d.ts @@ -52,4 +52,14 @@ declare module 'ass-json' { version: string homepage: string } + + interface ServerSideEmbed { + title: string + description?: string + author?: string + authorUrl?: string + provider?: string + providerUrl?: string + color?: string + } } From 7370303b7a7123c0b154e121619b000052c0c0e1 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 20:40:47 -0700 Subject: [PATCH 44/59] docs: explain how to use server-side embeds --- .github/README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 5acbfcc8..ead4e41a 100644 --- a/.github/README.md +++ b/.github/README.md @@ -247,7 +247,7 @@ If you primarily share media on Discord, you can add these additional (optional) | Header | Purpose | | ------ | ------- | -| **`X-Ass-OG-Title`** | Large text shown above your media | +| **`X-Ass-OG-Title`** | Large text shown above your media. Required for embeds to appear on desktop. | | **`X-Ass-OG-Description`** | Small text shown below the title but above the media (does not show up on videos) | | **`X-Ass-OG-Author`** | Small text shown above the title | | **`X-Ass-OG-Author-Url`** | URL to open when the Author is clicked | @@ -265,6 +265,20 @@ You can insert certain metadata into your embeds with these placeholders: | **`&filename`** | The original filename of the uploaded file | | **`×tamp`** | The timestamp of when the file was uploaded (example: `Oct 14, 1983, 1:30 PM`) | +#### Server-side embed configuration + +You may also specify a default embed config on the server. Keep in mind that if users specify the `X-Ass-OG-Title` header, the server-side config will be ignored. To configure the server-side embed, create a new file in the `share/` directory named `embed.json`. Available options are: + +- **`title`** +- `description` +- `author` +- `authorUrl` +- `provider` +- `providerUrl` +- `color` + +Their values are equivalent to the headers listed above. + ### Webhooks You may use Discord webhooks as an easy way to keep track of your uploads. The first step is to [create a new Webhook]. You only need to follow the first section, **Making a Webhook**. Once you are done that, click **Copy Webhook URL**. Finally, add these headers to your custom uploader: From faaaa8d572a781fbf60645d5ea7fd0374721d316 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:04:50 -0700 Subject: [PATCH 45/59] fix: this shouldn't be an entire `User`, that makes no sense --- src/auth.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 0afac37b..7b7d1d79 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -94,9 +94,7 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn /** * This is a WIP */ -export const createNewUser = (username: string, password: string, admin: boolean, meta?: { [key: string]: User }): Promise => new Promise(async (resolve, reject) => { - - // todo: finish this +export const createNewUser = (username: string, password: string, admin: boolean, meta?: { [key: string]: any }): Promise => new Promise(async (resolve, reject) => { // Create a new user object const newUser: User = { From 4663ce40c971fc6bbc3fd3f06eb561e39e959b5a Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:05:32 -0700 Subject: [PATCH 46/59] feat: add creating users via API --- src/auth.ts | 4 +++- src/routers/api.ts | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 7b7d1d79..384fabe3 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -113,7 +113,9 @@ export const createNewUser = (username: string, password: string, admin: boolean const authPath = path('auth.json'); const authData = fs.readJsonSync(authPath) as Users; authData.users.push(newUser); - fs.writeJson(authPath, authData, { spaces: '\t' }); + fs.writeJson(authPath, authData, { spaces: '\t' }) + .then(() => resolve(newUser)) + .catch(reject); }); export const setUserPassword = (unid: string, password: string): Promise => new Promise(async (resolve, reject) => { diff --git a/src/routers/api.ts b/src/routers/api.ts index a7a0675a..b15bb9dd 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -5,7 +5,8 @@ */ import { Router, Request, Response, NextFunction } from 'express'; -import { findFromToken, setUserPassword, users } from '../auth'; +import { findFromToken, setUserPassword, users, createNewUser } from '../auth'; +import { log } from '../utils'; import { data } from '../data'; import { User } from '../types/auth'; @@ -56,6 +57,23 @@ function buildUserRouter() { .catch(() => res.sendStatus(500)); }); + // Create a new user + // Admin only + userRouter.post('/new', adminAuthMiddleware, (req: Request, res: Response) => { + const username: string | undefined = req.body.username; + const password: string | undefined = req.body.password; + const admin = req.body.admin ?? false; + const meta: any = req.body.meta ?? {}; + + // Block if username or password is empty, or if username is already taken + if (username == null || username.length === 0 || password == null || password.length == 0 || users.find(user => user.username === username)) + return res.sendStatus(400); + + createNewUser(username, password, admin, meta) + .then((user) => res.send(user)) + .catch((err) => (log.error(err), res.sendStatus(500))); + }); + // Get a user (must be last as it's a catch-all) // Admin only userRouter.get('/:id', adminAuthMiddleware, (req: Request, res: Response) => From f441844ced0de218058866d876c0f73a62867e11 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:08:03 -0700 Subject: [PATCH 47/59] docs: detail adding new users via API --- .github/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index ead4e41a..09d44b60 100644 --- a/.github/README.md +++ b/.github/README.md @@ -382,8 +382,10 @@ Other things to note: | **`GET /user/all`** | Returns a list of all users | Yes | | **`GET /user/self`** | Returns the current user | No | | **`GET /user/token/:token`** | Returns the user with the given token | No | -| **`POST /user/reset`** | Resets the current user's **password** (token resets coming soon) | No | +| **`POST /user/reset`** | Resets the current user's **password** (token resets coming soon). Request body must be a JSON object including `username` and `password`. | No | | **`GET /user/:id`** | Returns the user with the given ID | Yes | +| **`POST /user/new`** | Creates a new user. Request body must be a JSON object including `username` and `password`. You may optionally include `admin` (boolean) or `meta` (object). Returns 400 if fails. | Yes | + ## Custom frontends - OUTDATED From 371e5fc5fa0f5a4e91fb79355c4db8e2583f9b08 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:10:48 -0700 Subject: [PATCH 48/59] feat: remove adding users via CLI temporarily --- .github/README.md | 7 +------ package.json | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/README.md b/.github/README.md index 09d44b60..791652e9 100644 --- a/.github/README.md +++ b/.github/README.md @@ -199,10 +199,6 @@ For HTTPS support, you must configure a reverse proxy. I recommend Caddy but any [Caddy]: https://caddyserver.com/ [my tutorial]: https://old.jmoore.dev/tutorials/2021/03/caddy-express-reverse-proxy/ -## Generating new tokens - -If you need to generate a new token at any time, run `npm run new-token `. This will **automatically** load the new token so there is no need to restart ass. Username field is optional; if left blank, a random username will be created. - ## Cloudflare users In your Cloudflare DNS dashboard, set your domain/subdomain to **DNS Only** if you experience issues with **Proxied**. @@ -355,7 +351,7 @@ S3 servers are generally very fast & have very good uptime, though this will dep The user system was overhauled in v0.14.0 to allow more features and flexibility. New fields on users include `admin`, `passhash`, `unid`, and `meta` (these will be documented more once the system is finalized). -ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. Adding new users via `npm run new-token ` should work as expected, though you'll need to re-launch ass to load the new file. +ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. Adding new users via `npm run new-token ` is currently not supported. Please see the API below for how to add a new user via the API. **Things still borked:** @@ -447,7 +443,6 @@ ass has a number of pre-made npm scripts for you to use. **All** of these script | `setup` | Starts the easy setup process. Should be run after any updates that introduce new config options. | | `metrics` | Runs the metrics script. This is a simple script that outputs basic resource statistics. | | `purge` | Purges all uploads & data associated with them. This does **not** delete any users, however. | -| `new-token` | Generates a new API token. Accepts one parameter for specifying a username, like `npm run new-token `. ass automatically detects the new token & reloads it, so there's no need to restart the server. | | `engine-check` | Ensures your environment meets the minimum Node & npm version requirements. | [`FORCE_COLOR`]: https://nodejs.org/dist/latest-v16.x/docs/api/cli.html#cli_force_color_1_2_3 diff --git a/package.json b/package.json index ddfdca47..8c6ecd70 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "start": "node dist/ass.js", "setup": "node dist/setup.js", "metrics": "node dist/metrics.js", - "new-token": "node dist/generators/token.js", "engine-check": "node dist/checkEngine.js", "prestart": "npm run engine-check", "presetup": "npm run engine-check", From 7b6e2b12e176cdfcd0db4a9c504116838fdb6e14 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:28:15 -0700 Subject: [PATCH 49/59] feat: create default users --- src/auth.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 384fabe3..4429ecce 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -92,7 +92,7 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn }); /** - * This is a WIP + * Creates a new user account */ export const createNewUser = (username: string, password: string, admin: boolean, meta?: { [key: string]: any }): Promise => new Promise(async (resolve, reject) => { @@ -112,7 +112,12 @@ export const createNewUser = (username: string, password: string, admin: boolean // Save the new user to auth.json const authPath = path('auth.json'); const authData = fs.readJsonSync(authPath) as Users; + + if (!authData.users) authData.users = []; authData.users.push(newUser); + + if (!authData.meta) authData.meta = {}; + fs.writeJson(authPath, authData, { spaces: '\t' }) .then(() => resolve(newUser)) .catch(reject); @@ -166,16 +171,16 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) migrate(authFile)); else return json; }) - .then((json: Users) => { + .then(async (json) => { // Check if the file is empty - if (Object.keys(json).length === 0) { + if (!json.users || json.users.length === 0) { log.debug('auth.json is empty, creating default user'); - //return createDefaultUser(); // todo: need to do this + return await createNewUser('ass', nanoid(), true); } // Add users to the map - json.users.forEach((user) => users.push(user)); + return json.users.forEach((user) => users.push(user)); }) .catch((errReadJson) => log.error('Failed to read auth.json').callback(reject, errReadJson)) .then(resolve); From 63a06d004dc335937c6209b60cf3cd0355ace8a1 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:28:32 -0700 Subject: [PATCH 50/59] docs: update notes on what is not added yet --- .github/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/README.md b/.github/README.md index 791652e9..05bc7f97 100644 --- a/.github/README.md +++ b/.github/README.md @@ -351,12 +351,13 @@ S3 servers are generally very fast & have very good uptime, though this will dep The user system was overhauled in v0.14.0 to allow more features and flexibility. New fields on users include `admin`, `passhash`, `unid`, and `meta` (these will be documented more once the system is finalized). +New installs will automatically generate a default user. Check the `auth.json` file for the token. You will use this for API requests and to authenticate within ShareX. + ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. Adding new users via `npm run new-token ` is currently not supported. Please see the API below for how to add a new user via the API. -**Things still borked:** +**Things still not added:** -- Creating a default user on new installs -- Creating/modifying/deleting users via the API +- Modifying/deleting users via the API - The filewatcher that reloads `auth.json` when modified on CLI (to be changed in the future) ## Developer API From 93dc91a761035301e20472537db37ccf788d40ae Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 21:50:59 -0700 Subject: [PATCH 51/59] fix: tested password reset in prod, not working for some reason --- src/auth.ts | 4 +++- src/routers/api.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 4429ecce..8abc4035 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -137,7 +137,9 @@ export const setUserPassword = (unid: string, password: string): Promise = const authData = fs.readJsonSync(authPath) as Users; const userIndex = authData.users.findIndex((user) => user.unid === unid); authData.users[userIndex] = user; - fs.writeJson(authPath, authData, { spaces: '\t' }); + fs.writeJson(authPath, authData, { spaces: '\t' }) + .then(() => resolve(user)) + .catch(reject); }); /** diff --git a/src/routers/api.ts b/src/routers/api.ts index b15bb9dd..142f6335 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -54,7 +54,7 @@ function buildUserRouter() { setUserPassword(id, newPassword) .then(() => res.sendStatus(200)) - .catch(() => res.sendStatus(500)); + .catch((err) => (log.error(err), res.sendStatus(500))); }); // Create a new user From 084b5fba3ac2e5665ca250590e92e3b6c7cccf65 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sat, 24 Dec 2022 22:30:12 -0700 Subject: [PATCH 52/59] fix: run this sequentially to avoid crashing on low-spec servers, maybe --- src/auth.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 8abc4035..d5fbc125 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -70,20 +70,37 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn // Migrate the datafile (token => uploader) .then(() => data().get()) - .then((fileData: [string, FileData][]) => + .then((fileData: [string, FileData][]) => { + + // ! A note about this block. + // I know it's gross. But using Promise.all crashes low-spec servers, so I had to do it this way. Sorry. + // Thanks to CoPilot for writing `runQueue` :D // Wait for all the deletions and puts to finish - Promise.all(fileData.map(async ([key, file]) => { + return new Promise((resolve, reject) => { + + // Create a queue of functions to run + const queue = fileData.map(([key, file]) => async () => { + + // We need to use `newUsers` because `users` hasn't been re-assigned yet + const user = newUsers.users.find((user) => user.token === file.token!)?.unid ?? ''; // ? This is probably fine - // We need to use `newUsers` because `users` hasn't been re-assigned yet - const user = newUsers.users.find((user) => user.token === file.token!)?.unid ?? ''; // ? This is probably fine + // Because of the stupid way I wrote papito, we need to DEL before we can PUT + await data().del(key); - // Because of the stupid way I wrote papito, we need to DEL before we can PUT - await data().del(key); + // PUT the new data + return data().put(key, { ...file, uploader: user }); + }); - // PUT the new data - return data().put(key, { ...file, uploader: user }); - }))) + // Recursively run the queue, hopefully sequentially without running out of memory + const runQueue = (index: number) => { + if (index >= queue.length) return resolve(void 0); + queue[index]().then(() => runQueue(index + 1)).catch(reject); + }; + + runQueue(0); + }); + }) // We did it hoofuckingray .then(() => log.success('Migrated all auth & file data to new auth system')) From eb5578ef9ee8595e18bdd6d46269801ef44e72c8 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 09:47:04 -0700 Subject: [PATCH 53/59] chore: minor cleanup before release --- src/auth.ts | 19 +++++++++++++------ src/routers/api.ts | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index d5fbc125..dac35a04 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -16,8 +16,8 @@ const SALT_ROUNDS = 10; /** * !!!!! * Things for tycrek to do: - * - [ ] Add a way to configure passwords - * - [ ] Create new users + * - [x] Add a way to configure passwords + * - [x] Create new users * - [ ] Modify user (admin, meta, replace token/token history) * - [ ] Delete user * - [x] Get user @@ -70,14 +70,14 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn // Migrate the datafile (token => uploader) .then(() => data().get()) - .then((fileData: [string, FileData][]) => { + .then((fileData: [string, FileData][]) => // ! A note about this block. // I know it's gross. But using Promise.all crashes low-spec servers, so I had to do it this way. Sorry. // Thanks to CoPilot for writing `runQueue` :D // Wait for all the deletions and puts to finish - return new Promise((resolve, reject) => { + new Promise((resolve, reject) => { // Create a queue of functions to run const queue = fileData.map(([key, file]) => async () => { @@ -99,8 +99,7 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn }; runQueue(0); - }); - }) + })) // We did it hoofuckingray .then(() => log.success('Migrated all auth & file data to new auth system')) @@ -110,6 +109,7 @@ const migrate = (authFileName = 'auth.json'): Promise => new Promise(asyn /** * Creates a new user account + * @since v0.14.0 */ export const createNewUser = (username: string, password: string, admin: boolean, meta?: { [key: string]: any }): Promise => new Promise(async (resolve, reject) => { @@ -140,6 +140,10 @@ export const createNewUser = (username: string, password: string, admin: boolean .catch(reject); }); +/** + * Sets the password for a user + * @since v0.14.0 + */ export const setUserPassword = (unid: string, password: string): Promise => new Promise(async (resolve, reject) => { // Find the user @@ -161,6 +165,7 @@ export const setUserPassword = (unid: string, password: string): Promise = /** * Called by ass.ts on startup + * @since v0.14.0 */ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) => { // Reset user array (https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript#1232046) @@ -207,6 +212,7 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) /** * Retrieves a user using their upload token. Returns `null` if the user does not exist. + * @since v0.14.0 */ export const findFromToken = (token: string) => { return users.find((user) => user.token === token) || null; @@ -214,6 +220,7 @@ export const findFromToken = (token: string) => { /** * Verifies that the upload token in the request exists in the user map + * @since v0.14.0 */ export const verifyValidToken = (req: Request) => { return req.headers.authorization && findFromToken(req.headers.authorization); diff --git a/src/routers/api.ts b/src/routers/api.ts index 142f6335..13f9f936 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -17,6 +17,7 @@ const RouterApi = Router(); /** * Token authentication middleware for Admins + * @since v0.14.0 */ const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => { const user = findFromToken(req.headers.authorization ?? ''); @@ -25,6 +26,7 @@ const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => /** * Simple function to either return JSON or a 404, so I don't have to write it 40 times. + * @since v0.14.0 */ const userFinder = (res: Response, user: User | undefined) => user ? res.json(user) : res.sendStatus(404); From 182d674a7e6a3b1bfd79230b619a6608be1e8d57 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:38:52 -0700 Subject: [PATCH 54/59] feat: add a CLI key for scripts to auth with API --- src/auth.ts | 15 +++++++++++++++ src/routers/api.ts | 4 ++-- src/types/auth.d.ts | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index dac35a04..74fea290 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -203,6 +203,13 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) return await createNewUser('ass', nanoid(), true); } + // Check if the CLI key is set + if (!json.cliKey || json.cliKey.length === 0) { + log.debug('CLI key is not set, generating new key'); + json.cliKey = nanoid(32); + fs.writeJsonSync(file, json, { spaces: '\t' }); + } + // Add users to the map return json.users.forEach((user) => users.push(user)); }) @@ -224,6 +231,14 @@ export const findFromToken = (token: string) => { */ export const verifyValidToken = (req: Request) => { return req.headers.authorization && findFromToken(req.headers.authorization); + +/** + * Verifies that the CLI key in the request matches the one in auth.json + * @since v0.14.0 + */ +export const verifyCliKey = (req: Request) => { + const cliKey: string = fs.readJsonSync(path('auth.json')).cliKey; + return req.headers.authorization != null && req.headers.authorization === cliKey; }; // todo: move inside of onStart (currently broken) diff --git a/src/routers/api.ts b/src/routers/api.ts index 13f9f936..fb45e921 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -5,7 +5,7 @@ */ import { Router, Request, Response, NextFunction } from 'express'; -import { findFromToken, setUserPassword, users, createNewUser } from '../auth'; +import { findFromToken, setUserPassword, users, createNewUser, verifyCliKey } from '../auth'; import { log } from '../utils'; import { data } from '../data'; import { User } from '../types/auth'; @@ -21,7 +21,7 @@ const RouterApi = Router(); */ const adminAuthMiddleware = (req: Request, res: Response, next: NextFunction) => { const user = findFromToken(req.headers.authorization ?? ''); - (user && user.admin) ? next() : res.sendStatus(401); + (verifyCliKey(req) || (user && user.admin)) ? next() : res.sendStatus(401); }; /** diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts index 6234ac89..acabc7af 100644 --- a/src/types/auth.d.ts +++ b/src/types/auth.d.ts @@ -49,6 +49,11 @@ export interface Users { */ migrated?: boolean + /** + * Access key for the CLI + */ + cliKey?: string + /** * Extra metadata. Frontends can use this to store extra data. */ From 2abea4894d0d38e994f1e0717c6fdd9fecbb9f88 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:39:19 -0700 Subject: [PATCH 55/59] chore: minor cleanup --- src/auth.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 74fea290..eb4000b2 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -136,6 +136,7 @@ export const createNewUser = (username: string, password: string, admin: boolean if (!authData.meta) authData.meta = {}; fs.writeJson(authPath, authData, { spaces: '\t' }) + .then(() => log.info('Created new user', newUser.username, newUser.unid)) .then(() => resolve(newUser)) .catch(reject); }); @@ -221,16 +222,13 @@ export const onStart = (authFile = 'auth.json') => new Promise((resolve, reject) * Retrieves a user using their upload token. Returns `null` if the user does not exist. * @since v0.14.0 */ -export const findFromToken = (token: string) => { - return users.find((user) => user.token === token) || null; -}; +export const findFromToken = (token: string) => users.find((user) => user.token === token) || null; /** * Verifies that the upload token in the request exists in the user map * @since v0.14.0 */ -export const verifyValidToken = (req: Request) => { - return req.headers.authorization && findFromToken(req.headers.authorization); +export const verifyValidToken = (req: Request) => req.headers.authorization && findFromToken(req.headers.authorization); /** * Verifies that the CLI key in the request matches the one in auth.json From d3181cb1f8e2054356fa8a8f28b141019b3a8313 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:39:30 -0700 Subject: [PATCH 56/59] fix: new tokens may have `_` or `-` --- src/routers/upload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/upload.ts b/src/routers/upload.ts index 0a5cddbf..f04ebd49 100644 --- a/src/routers/upload.ts +++ b/src/routers/upload.ts @@ -37,7 +37,7 @@ bb.extend(router, { // Block unauthorized requests and attempt token sanitization router.post('/', (req: Request, res: Response, next: Function) => { req.headers.authorization = req.headers.authorization || ''; - req.token = req.headers.authorization.replace(/[^\da-z]/gi, ''); // Strip anything that isn't a digit or ASCII letter + req.token = req.headers.authorization.replace(/[^\da-z_-]/gi, ''); // Strip anything that isn't a digit, ASCII letter, or underscore/hyphen !verifyValidToken(req) ? log.warn('Upload blocked', 'Unauthorized').callback(() => res.sendStatus(CODE_UNAUTHORIZED)) : next(); // skipcq: JS-0093 }); From 69bf9aaa8617691d43c0a11bf408382eb25c68f0 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:39:48 -0700 Subject: [PATCH 57/59] feat: added script to add users by CLI --- package.json | 3 ++- src/tools/script.adduser.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/tools/script.adduser.ts diff --git a/package.json b/package.json index 8c6ecd70..6c78ad0a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "docker-upfull": "npm run docker-update && npm run docker-resetup", "docker-resetup": "docker-compose exec ass npm run setup && docker-compose restart", "cli-setpassword": "node dist/tools/script.setpassword.js", - "cli-testpassword": "node dist/tools/script.testpassword.js" + "cli-testpassword": "node dist/tools/script.testpassword.js", + "cli-adduser": "node dist/tools/script.adduser.js" }, "repository": "github:tycrek/ass", "keywords": [ diff --git a/src/tools/script.adduser.ts b/src/tools/script.adduser.ts new file mode 100644 index 00000000..95c61df7 --- /dev/null +++ b/src/tools/script.adduser.ts @@ -0,0 +1,29 @@ +import path from 'path'; +import fs from 'fs-extra'; +import axios from 'axios'; +import logger from '../logger'; +import { User } from '../types/auth'; + +// Port from config.json +const { port } = fs.readJsonSync(path.join(process.cwd(), 'config.json')); + +// CLI key from auth.json +const { cliKey } = fs.readJsonSync(path.join(process.cwd(), 'auth.json')); + +if (process.argv.length < 4) { + logger.error('Missing username or password'); + logger.error('Usage: node script.adduser.js [admin] [meta]'); + process.exit(1); +} else { + const username = process.argv[2]; + const password = process.argv[3]; + const admin = process.argv[4] ? process.argv[4].toLowerCase() === 'true' : false; + const meta = process.argv[5] ? JSON.parse(process.argv[5]) : {}; + + axios.post(`http://localhost:${port}/api/user/new`, { username, password, admin, meta }, { headers: { 'Authorization': cliKey } }) + .then((response) => { + const user = response.data as User; + logger.info('User created', username, user.unid).callback(() => process.exit(0)) + }) + .catch((err) => logger.error(err).callback(() => process.exit(1))); +} From e89669321994813ae1440ec8580820269d63370d Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:45:36 -0700 Subject: [PATCH 58/59] docs: added notes on adding users via CLI --- .github/README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/README.md b/.github/README.md index 8d511018..4a80f3f9 100644 --- a/.github/README.md +++ b/.github/README.md @@ -354,12 +354,28 @@ The user system was overhauled in v0.14.0 to allow more features and flexibility New installs will automatically generate a default user. Check the `auth.json` file for the token. You will use this for API requests and to authenticate within ShareX. -ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. Adding new users via `npm run new-token ` is currently not supported. Please see the API below for how to add a new user via the API. +ass will automatically convert your old `auth.json` to the new format. **Always backup your `auth.json` and `data.json` before updating**. By default, the original user (named `ass`) will be marked as an admin. + +### Adding users + +You may add users via the CLI or the API. I'll document the API further in the future. + +#### CLI + +```bash +npm run cli-adduser [admin] [meta] +``` + +| Argument | Purpose | +| -------- | ------- | +| **`username`** `string` | The username of the user. | +| **`password`** `string` | The password of the user. | +| **`admin?`** `boolean` | Whether the user is an admin. Defaults to `false`. | +| **`meta?`** `string` | Any additional metadata to store on the user, as a JSON string. | **Things still not added:** - Modifying/deleting users via the API -- The filewatcher that reloads `auth.json` when modified on CLI (to be changed in the future) ## Developer API From 94cac0f5baa9e107b9f477a38e1702ac1c1f6966 Mon Sep 17 00:00:00 2001 From: tycrek Date: Sun, 25 Dec 2022 10:45:52 -0700 Subject: [PATCH 59/59] chore: remove old code --- src/auth.ts | 16 +--------------- src/utils.ts | 5 ----- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index eb4000b2..8b1b3921 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -6,7 +6,7 @@ import fs from 'fs-extra'; import { nanoid } from 'nanoid'; import { Request } from 'express'; import bcrypt from 'bcrypt'; -import { log, path, arrayEquals } from './utils'; +import { log, path } from './utils'; import { data } from './data'; import { User, Users, OldUsers } from './types/auth'; import { FileData } from './types/definitions'; @@ -238,17 +238,3 @@ export const verifyCliKey = (req: Request) => { const cliKey: string = fs.readJsonSync(path('auth.json')).cliKey; return req.headers.authorization != null && req.headers.authorization === cliKey; }; - -// todo: move inside of onStart (currently broken) -// Monitor auth.json for changes (triggered by running 'npm run new-token') -/* fs.watch(path('auth.json'), { persistent: false }, - (eventType: String) => eventType === 'change' && fs.readJson(path('auth.json')) - .then((json: { users: JSON[] }) => { - if (!(arrayEquals(Object.keys(users), Object.keys(json.users)))) { - // @ts-ignore - Object.keys(json.users).forEach((token) => (!Object.prototype.hasOwnProperty.call(users, token)) && (users[token] = json.users[token])); - log.info('New token added', Object.keys(users)[Object.keys(users).length - 1] || 'No new token'); - } - }) - .catch(console.error)); - */ \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 934f828a..abdb81a3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -68,10 +68,6 @@ export function replaceholder(data: string, size: number, timestamp: number, tim .replace(/×tamp/g, formatTimestamp(timestamp, timeoffset)); } -export function arrayEquals(arr1: any[], arr2: any[]) { - return arr1.length === arr2.length && arr1.slice().sort().every((value: string, index: number) => value === arr2.slice().sort()[index]) -} - const idModes = { zws: 'zws', // Zero-width spaces (see: https://zws.im/) og: 'original', // Use original uploaded filename @@ -116,7 +112,6 @@ module.exports = { }), generateToken: () => token(), generateId, - arrayEquals, downloadTempS3: (file: FileData) => new Promise((resolve: Function, reject) => fetch(getS3url(file.randomId, file.ext)) .then((f2) => f2.body!.pipe(fs.createWriteStream(Path.join(__dirname, diskFilePath, sanitize(file.originalname))).on('close', () => resolve())))