mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Removed error message on URL input on the home page Fixed bug that prevented file deletion in multi user mode with archiving enabled
540 lines
17 KiB
JavaScript
540 lines
17 KiB
JavaScript
const path = require('path');
|
|
const config_api = require('../config');
|
|
const consts = require('../consts');
|
|
var subscriptions_api = require('../subscriptions')
|
|
const fs = require('fs-extra');
|
|
var jwt = require('jsonwebtoken');
|
|
const { uuid } = require('uuidv4');
|
|
var bcrypt = require('bcrypt');
|
|
|
|
|
|
var LocalStrategy = require('passport-local').Strategy;
|
|
var JwtStrategy = require('passport-jwt').Strategy,
|
|
ExtractJwt = require('passport-jwt').ExtractJwt;
|
|
|
|
// other required vars
|
|
let logger = null;
|
|
var users_db = null;
|
|
let SERVER_SECRET = null;
|
|
let JWT_EXPIRATION = null;
|
|
let opts = null;
|
|
let saltRounds = null;
|
|
|
|
exports.initialize = function(input_users_db, input_logger) {
|
|
setLogger(input_logger)
|
|
setDB(input_users_db);
|
|
|
|
/*************************
|
|
* Authentication module
|
|
************************/
|
|
saltRounds = 10;
|
|
|
|
JWT_EXPIRATION = (60 * 60); // one hour
|
|
|
|
SERVER_SECRET = null;
|
|
if (users_db.get('jwt_secret').value()) {
|
|
SERVER_SECRET = users_db.get('jwt_secret').value();
|
|
} else {
|
|
SERVER_SECRET = uuid();
|
|
users_db.set('jwt_secret', SERVER_SECRET).write();
|
|
}
|
|
|
|
opts = {}
|
|
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
|
|
opts.secretOrKey = SERVER_SECRET;
|
|
/*opts.issuer = 'example.com';
|
|
opts.audience = 'example.com';*/
|
|
|
|
exports.passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
|
|
const user = users_db.get('users').find({uid: jwt_payload.user}).value();
|
|
if (user) {
|
|
return done(null, user);
|
|
} else {
|
|
return done(null, false);
|
|
// or you could create a new account
|
|
}
|
|
}));
|
|
}
|
|
|
|
function setLogger(input_logger) {
|
|
logger = input_logger;
|
|
}
|
|
|
|
function setDB(input_users_db) {
|
|
users_db = input_users_db;
|
|
}
|
|
|
|
exports.passport = require('passport');
|
|
|
|
exports.passport.serializeUser(function(user, done) {
|
|
done(null, user);
|
|
});
|
|
|
|
exports.passport.deserializeUser(function(user, done) {
|
|
done(null, user);
|
|
});
|
|
|
|
/***************************************
|
|
* Register user with hashed password
|
|
**************************************/
|
|
exports.registerUser = function(req, res) {
|
|
var userid = req.body.userid;
|
|
var username = req.body.username;
|
|
var plaintextPassword = req.body.password;
|
|
|
|
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
|
res.sendStatus(409);
|
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
|
return;
|
|
}
|
|
|
|
bcrypt.hash(plaintextPassword, saltRounds)
|
|
.then(function(hash) {
|
|
let new_user = {
|
|
name: username,
|
|
uid: userid,
|
|
passhash: hash,
|
|
files: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
playlists: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
subscriptions: [],
|
|
created: Date.now(),
|
|
role: userid === 'admin' ? 'admin' : 'user',
|
|
permissions: [],
|
|
permission_overrides: []
|
|
};
|
|
// check if user exists
|
|
if (users_db.get('users').find({uid: userid}).value()) {
|
|
// user id is taken!
|
|
logger.error('Registration failed: UID is already taken!');
|
|
res.status(409).send('UID is already taken!');
|
|
} else if (users_db.get('users').find({name: username}).value()) {
|
|
// user name is taken!
|
|
logger.error('Registration failed: User name is already taken!');
|
|
res.status(409).send('User name is already taken!');
|
|
} else {
|
|
// add to db
|
|
users_db.get('users').push(new_user).write();
|
|
logger.verbose(`New user created: ${new_user.name}`);
|
|
res.send({
|
|
user: new_user
|
|
});
|
|
}
|
|
})
|
|
.then(function(result) {
|
|
|
|
})
|
|
.catch(function(err) {
|
|
logger.error(err);
|
|
if( err.code == 'ER_DUP_ENTRY' ) {
|
|
res.status(409).send('UserId already taken');
|
|
} else {
|
|
res.sendStatus(409);
|
|
}
|
|
});
|
|
}
|
|
|
|
/***************************************
|
|
* Login methods
|
|
**************************************/
|
|
|
|
/*************************************************
|
|
* This gets called when passport.authenticate()
|
|
* gets called.
|
|
*
|
|
* This checks that the credentials are valid.
|
|
* If so, passes the user info to the next middleware.
|
|
************************************************/
|
|
|
|
|
|
exports.passport.use(new LocalStrategy({
|
|
usernameField: 'userid',
|
|
passwordField: 'password'},
|
|
function(username, password, done) {
|
|
const user = users_db.get('users').find({name: username}).value();
|
|
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
|
if (user) {
|
|
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
|
}
|
|
}
|
|
));
|
|
|
|
/*passport.use(new BasicStrategy(
|
|
function(userid, plainTextPassword, done) {
|
|
const user = users_db.get('users').find({name: userid}).value();
|
|
if (user) {
|
|
var hashedPwd = user.passhash;
|
|
return bcrypt.compare(plainTextPassword, hashedPwd);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
));
|
|
*/
|
|
|
|
/*************************************************************
|
|
* This is a wrapper for auth.passport.authenticate().
|
|
* We use this to change WWW-Authenticate header so
|
|
* the browser doesn't pop-up challenge dialog box by default.
|
|
* Browser's will pop-up up dialog when status is 401 and
|
|
* "WWW-Authenticate:Basic..."
|
|
*************************************************************/
|
|
/*
|
|
exports.authenticateViaPassport = function(req, res, next) {
|
|
exports.passport.authenticate('basic',{session:false},
|
|
function(err, user, info) {
|
|
if(!user){
|
|
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
|
|
res.status(401).send('Invalid Authentication');
|
|
} else {
|
|
req.user = user;
|
|
next();
|
|
}
|
|
}
|
|
)(req, res, next);
|
|
};
|
|
*/
|
|
|
|
/**********************************
|
|
* Generating/Signing a JWT token
|
|
* And attaches the user info into
|
|
* the payload to be sent on every
|
|
* request.
|
|
*********************************/
|
|
exports.generateJWT = function(req, res, next) {
|
|
var payload = {
|
|
exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
|
|
, user: req.user.uid
|
|
};
|
|
req.token = jwt.sign(payload, SERVER_SECRET);
|
|
next();
|
|
}
|
|
|
|
exports.returnAuthResponse = function(req, res) {
|
|
res.status(200).json({
|
|
user: req.user,
|
|
token: req.token,
|
|
permissions: exports.userPermissions(req.user.uid),
|
|
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
|
});
|
|
}
|
|
|
|
/***************************************
|
|
* Authorization: middleware that checks the
|
|
* JWT token for validity before allowing
|
|
* the user to access anything.
|
|
*
|
|
* It also passes the user object to the next
|
|
* middleware through res.locals
|
|
**************************************/
|
|
exports.ensureAuthenticatedElseError = function(req, res, next) {
|
|
var token = getToken(req.query);
|
|
if( token ) {
|
|
try {
|
|
var payload = jwt.verify(token, SERVER_SECRET);
|
|
// console.log('payload: ' + JSON.stringify(payload));
|
|
// check if user still exists in database if you'd like
|
|
res.locals.user = payload.user;
|
|
next();
|
|
} catch(err) {
|
|
res.status(401).send('Invalid Authentication');
|
|
}
|
|
} else {
|
|
res.status(401).send('Missing Authorization header');
|
|
}
|
|
}
|
|
|
|
// change password
|
|
exports.changeUserPassword = async function(user_uid, new_pass) {
|
|
return new Promise(resolve => {
|
|
bcrypt.hash(new_pass, saltRounds)
|
|
.then(function(hash) {
|
|
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
|
|
resolve(true);
|
|
}).catch(err => {
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
// change user permissions
|
|
exports.changeUserPermissions = function(user_uid, permission, new_value) {
|
|
try {
|
|
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
|
user_db_obj.get('permissions').pull(permission).write();
|
|
user_db_obj.get('permission_overrides').pull(permission).write();
|
|
if (new_value === 'yes') {
|
|
user_db_obj.get('permissions').push(permission).write();
|
|
user_db_obj.get('permission_overrides').push(permission).write();
|
|
} else if (new_value === 'no') {
|
|
user_db_obj.get('permission_overrides').push(permission).write();
|
|
}
|
|
return true;
|
|
} catch (err) {
|
|
logger.error(err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// change role permissions
|
|
exports.changeRolePermissions = function(role, permission, new_value) {
|
|
try {
|
|
const role_db_obj = users_db.get('roles').get(role);
|
|
role_db_obj.get('permissions').pull(permission).write();
|
|
if (new_value === 'yes') {
|
|
role_db_obj.get('permissions').push(permission).write();
|
|
}
|
|
return true;
|
|
} catch (err) {
|
|
logger.error(err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
exports.adminExists = function() {
|
|
return !!users_db.get('users').find({uid: 'admin'}).value();
|
|
}
|
|
|
|
// video stuff
|
|
|
|
exports.getUserVideos = function(user_uid, type) {
|
|
const user = users_db.get('users').find({uid: user_uid}).value();
|
|
return user['files'][type];
|
|
}
|
|
|
|
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
|
if (!type) {
|
|
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
|
if (!file) {
|
|
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
|
if (file) type = 'video';
|
|
} else {
|
|
type = 'audio';
|
|
}
|
|
}
|
|
|
|
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
|
|
|
// prevent unauthorized users from accessing the file info
|
|
if (requireSharing && !file['sharingEnabled']) file = null;
|
|
|
|
return file;
|
|
}
|
|
|
|
exports.addPlaylist = function(user_uid, new_playlist, type) {
|
|
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
|
|
return true;
|
|
}
|
|
|
|
exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) {
|
|
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
|
|
return true;
|
|
}
|
|
|
|
exports.removePlaylist = function(user_uid, playlistID, type) {
|
|
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
|
|
return true;
|
|
}
|
|
|
|
exports.getUserPlaylists = function(user_uid, type) {
|
|
const user = users_db.get('users').find({uid: user_uid}).value();
|
|
return user['playlists'][type];
|
|
}
|
|
|
|
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
|
|
let playlist = null;
|
|
if (!type) {
|
|
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
|
|
if (!playlist) {
|
|
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
|
|
if (playlist) type = 'video';
|
|
} else {
|
|
type = 'audio';
|
|
}
|
|
}
|
|
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
|
|
|
|
// prevent unauthorized users from accessing the file info
|
|
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
|
|
|
return playlist;
|
|
}
|
|
|
|
exports.registerUserFile = function(user_uid, file_object, type) {
|
|
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
.remove({
|
|
path: file_object['path']
|
|
}).write();
|
|
|
|
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
.push(file_object)
|
|
.write();
|
|
}
|
|
|
|
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
|
|
let success = false;
|
|
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
|
if (file_obj) {
|
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
|
|
|
// close descriptors
|
|
if (config_api.descriptors[file_obj.id]) {
|
|
try {
|
|
for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) {
|
|
config_api.descriptors[file_obj.id][i].destroy();
|
|
}
|
|
} catch(e) {
|
|
|
|
}
|
|
}
|
|
|
|
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
|
|
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
.remove({
|
|
uid: file_uid
|
|
}).write();
|
|
if (fs.existsSync(full_path)) {
|
|
// remove json and file
|
|
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
|
|
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
|
|
let youtube_id = null;
|
|
if (fs.existsSync(json_path)) {
|
|
youtube_id = fs.readJSONSync(json_path).id;
|
|
fs.unlinkSync(json_path);
|
|
} else if (fs.existsSync(alternate_json_path)) {
|
|
youtube_id = fs.readJSONSync(alternate_json_path).id;
|
|
fs.unlinkSync(alternate_json_path);
|
|
}
|
|
|
|
fs.unlinkSync(full_path);
|
|
|
|
// do archive stuff
|
|
|
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
if (useYoutubeDLArchive) {
|
|
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
|
|
|
|
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
|
if (fs.existsSync(archive_path)) {
|
|
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
|
if (blacklistMode && line) {
|
|
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
|
// adds newline to the beginning of the line
|
|
line = '\n' + line;
|
|
fs.appendFileSync(blacklistPath, line);
|
|
}
|
|
} else {
|
|
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
|
fs.ensureFileSync(archive_path);
|
|
}
|
|
}
|
|
}
|
|
success = true;
|
|
} else {
|
|
success = false;
|
|
logger.warn(`User file ${file_uid} does not exist!`);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
|
|
let success = false;
|
|
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
|
if (user_db_obj.value()) {
|
|
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
|
|
if (file_db_obj.value()) {
|
|
success = true;
|
|
file_db_obj.assign({sharingEnabled: enabled}).write();
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
exports.userHasPermission = function(user_uid, permission) {
|
|
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
|
const role = user_obj['role'];
|
|
if (!role) {
|
|
// role doesn't exist
|
|
logger.error('Invalid role ' + role);
|
|
return false;
|
|
}
|
|
const role_permissions = (users_db.get('roles').value())['permissions'];
|
|
|
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
|
|
|
// check if user has a negative/positive override
|
|
if (user_has_explicit_permission && permission_in_overrides) {
|
|
// positive override
|
|
return true;
|
|
} else if (!user_has_explicit_permission && permission_in_overrides) {
|
|
// negative override
|
|
return false;
|
|
}
|
|
|
|
// no overrides, let's check if the role has the permission
|
|
if (role_permissions.includes(permission)) {
|
|
return true;
|
|
} else {
|
|
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
exports.userPermissions = function(user_uid) {
|
|
let user_permissions = [];
|
|
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
|
const role = user_obj['role'];
|
|
if (!role) {
|
|
// role doesn't exist
|
|
logger.error('Invalid role ' + role);
|
|
return null;
|
|
}
|
|
const role_permissions = users_db.get('roles').get(role).get('permissions').value()
|
|
|
|
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
|
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
|
|
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
|
|
|
// check if user has a negative/positive override
|
|
if (user_has_explicit_permission && permission_in_overrides) {
|
|
// positive override
|
|
user_permissions.push(permission);
|
|
} else if (!user_has_explicit_permission && permission_in_overrides) {
|
|
// negative override
|
|
continue;
|
|
}
|
|
|
|
// no overrides, let's check if the role has the permission
|
|
if (role_permissions.includes(permission)) {
|
|
user_permissions.push(permission);
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return user_permissions;
|
|
}
|
|
|
|
function getToken(queryParams) {
|
|
if (queryParams && queryParams.jwt) {
|
|
var parted = queryParams.jwt.split(' ');
|
|
if (parted.length === 2) {
|
|
return parted[1];
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}; |