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.
pull/177/head
tycrek 1 year ago
parent 7b88ee9638
commit 1a9fad7076
No known key found for this signature in database
GPG Key ID: FF8A54DCE404885A

@ -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}` : ''}`)

@ -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<string, User>();
/**
* Migrates the old auth.json format to the new one
* @since v0.14.0
*/
const migrate = (): Promise<Users> => 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<User> => 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'))

@ -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

@ -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 : '<token-only>';
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', '');
});
})

@ -12,11 +12,6 @@ declare global {
}
}
export interface User {
token: string
username: string
}
export interface FileData {
// Data from request file object
uuid?: string

@ -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];

Loading…
Cancel
Save