|
|
|
/**
|
|
|
|
* Used for global auth management
|
|
|
|
*/
|
|
|
|
|
|
|
|
import fs from 'fs-extra';
|
|
|
|
import { nanoid } from 'nanoid';
|
|
|
|
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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* !!!!!
|
|
|
|
* 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 = [] as User[];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Migrates the old auth.json format to the new one
|
|
|
|
* @since v0.14.0
|
|
|
|
*/
|
|
|
|
const migrate = (authFileName = 'auth.json'): Promise<Users> => new Promise(async (resolve, reject) => {
|
|
|
|
|
|
|
|
// Get ready to read the old auth.json file
|
|
|
|
const authPath = path(authFileName);
|
|
|
|
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
|
|
|
|
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,
|
|
|
|
passhash,
|
|
|
|
token,
|
|
|
|
admin,
|
|
|
|
meta: {}
|
|
|
|
};
|
|
|
|
|
|
|
|
newUsers.users.push(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, password: string, admin: boolean, meta?: { [key: string]: User }): Promise<User> => new Promise(async (resolve, reject) => {
|
|
|
|
|
|
|
|
// todo: finish this
|
|
|
|
|
|
|
|
// Create a new user object
|
|
|
|
const newUser: User = {
|
|
|
|
unid: nanoid(),
|
|
|
|
username,
|
|
|
|
passhash: await bcrypt.hash(password, SALT_ROUNDS),
|
|
|
|
token: nanoid(32),
|
|
|
|
admin,
|
|
|
|
meta: meta || {}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add the user to the users map
|
|
|
|
users.push(newUser);
|
|
|
|
|
|
|
|
// Save the new user to auth.json
|
|
|
|
const authPath = path('auth.json');
|
|
|
|
const authData = fs.readJsonSync(authPath) as Users;
|
|
|
|
authData.users.push(newUser);
|
|
|
|
fs.writeJson(authPath, authData, { spaces: '\t' });
|
|
|
|
});
|
|
|
|
|
|
|
|
export const setUserPassword = (unid: string, password: string): Promise<User> => 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, SALT_ROUNDS);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
*/
|
|
|
|
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(authFile));
|
|
|
|
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
|
|
|
|
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. Returns `null` if the user does not exist.
|
|
|
|
*/
|
|
|
|
export const findFromToken = (token: string) => {
|
|
|
|
return users.find((user) => user.token === token) || null;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Verifies that the upload token in the request exists in the user map
|
|
|
|
*/
|
|
|
|
export const verifyValidToken = (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'))
|
|
|
|
.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));
|