Spaces:
Build error
Build error
| const config_api = require('../config'); | |
| const CONSTS = require('../consts'); | |
| const logger = require('../logger'); | |
| const db_api = require('../db'); | |
| const jwt = require('jsonwebtoken'); | |
| const { v4: uuid } = require('uuid'); | |
| const bcrypt = require('bcryptjs'); | |
| const fs = require('fs-extra'); | |
| const path = require('path'); | |
| var LocalStrategy = require('passport-local').Strategy; | |
| var LdapStrategy = require('passport-ldapauth'); | |
| var JwtStrategy = require('passport-jwt').Strategy, | |
| ExtractJwt = require('passport-jwt').ExtractJwt; | |
| // other required vars | |
| let SERVER_SECRET = null; | |
| let JWT_EXPIRATION = null; | |
| let opts = null; | |
| let saltRounds = 10; | |
| exports.initialize = function () { | |
| /************************* | |
| * Authentication module | |
| ************************/ | |
| if (db_api.database_initialized) { | |
| setupRoles(); | |
| } else { | |
| db_api.database_initialized_bs.subscribe(init => { | |
| if (init) setupRoles(); | |
| }); | |
| } | |
| // Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813 | |
| JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration'); | |
| if (!(+JWT_EXPIRATION)) { | |
| logger.warn(`JWT expiration value improperly set to ${JWT_EXPIRATION}, auto setting to 1 day.`); | |
| JWT_EXPIRATION = 86400; | |
| } else { | |
| JWT_EXPIRATION = +JWT_EXPIRATION; | |
| } | |
| SERVER_SECRET = null; | |
| if (db_api.users_db.get('jwt_secret').value()) { | |
| SERVER_SECRET = db_api.users_db.get('jwt_secret').value(); | |
| } else { | |
| SERVER_SECRET = uuid(); | |
| db_api.users_db.set('jwt_secret', SERVER_SECRET).write(); | |
| } | |
| opts = {} | |
| opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt'); | |
| opts.secretOrKey = SERVER_SECRET; | |
| exports.passport.use(new JwtStrategy(opts, async function(jwt_payload, done) { | |
| const user = await db_api.getRecord('users', {uid: jwt_payload.user}); | |
| if (user) { | |
| return done(null, user); | |
| } else { | |
| return done(null, false); | |
| // or you could create a new account | |
| } | |
| })); | |
| } | |
| const setupRoles = async () => { | |
| const required_roles = { | |
| admin: { | |
| permissions: CONSTS.AVAILABLE_PERMISSIONS | |
| }, | |
| user: { | |
| permissions: [ | |
| 'filemanager', | |
| 'subscriptions', | |
| 'sharing' | |
| ] | |
| } | |
| } | |
| const role_keys = Object.keys(required_roles); | |
| for (let i = 0; i < role_keys.length; i++) { | |
| const role_key = role_keys[i]; | |
| const role_in_db = await db_api.getRecord('roles', {key: role_key}); | |
| if (!role_in_db) { | |
| // insert task metadata into table if missing | |
| await db_api.insertRecordIntoTable('roles', { | |
| key: role_key, | |
| permissions: required_roles[role_key]['permissions'] | |
| }); | |
| } | |
| } | |
| } | |
| 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 = async (userid, username, plaintextPassword) => { | |
| const hash = await bcrypt.hash(plaintextPassword, saltRounds); | |
| const new_user = generateUserObject(userid, username, hash); | |
| // check if user exists | |
| if (await db_api.getRecord('users', {uid: userid})) { | |
| // user id is taken! | |
| logger.error('Registration failed: UID is already taken!'); | |
| return null; | |
| } else if (await db_api.getRecord('users', {name: username})) { | |
| // user name is taken! | |
| logger.error('Registration failed: User name is already taken!'); | |
| return null; | |
| } else { | |
| // add to db | |
| await db_api.insertRecordIntoTable('users', new_user); | |
| logger.verbose(`New user created: ${new_user.name}`); | |
| return new_user; | |
| } | |
| } | |
| exports.deleteUser = async (uid) => { | |
| let success = false; | |
| let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); | |
| const user_folder = path.join(__dirname, usersFileFolder, uid); | |
| const user_db_obj = await db_api.getRecord('users', {uid: uid}); | |
| if (user_db_obj) { | |
| // user exists, let's delete | |
| await fs.remove(user_folder); | |
| await db_api.removeRecord('users', {uid: uid}); | |
| success = true; | |
| } else { | |
| logger.error(`Could not find user with uid ${uid}`); | |
| } | |
| return success; | |
| } | |
| /*************************************** | |
| * 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.login = async (username, password) => { | |
| // even if we're using LDAP, we still want users to be able to login using internal credentials | |
| const user = await db_api.getRecord('users', {name: username}); | |
| if (!user) { | |
| if (config_api.getConfigItem('ytdl_auth_method') === 'internal') logger.error(`User ${username} not found`); | |
| return false; | |
| } | |
| if (user.auth_method && user.auth_method !== 'internal') { return false } | |
| return await bcrypt.compare(password, user.passhash) ? user : false; | |
| } | |
| exports.passport.use(new LocalStrategy({ | |
| usernameField: 'username', | |
| passwordField: 'password'}, | |
| async function(username, password, done) { | |
| return done(null, await exports.login(username, password)); | |
| } | |
| )); | |
| var getLDAPConfiguration = function(req, callback) { | |
| const ldap_config = config_api.getConfigItem('ytdl_ldap_config'); | |
| const opts = {server: ldap_config}; | |
| callback(null, opts); | |
| }; | |
| exports.passport.use(new LdapStrategy(getLDAPConfiguration, | |
| async function(user, done) { | |
| // check if ldap auth is enabled | |
| const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap'; | |
| if (!ldap_enabled) return done(null, false); | |
| const user_uid = user.uid; | |
| let db_user = await db_api.getRecord('users', {uid: user_uid}); | |
| if (!db_user) { | |
| // generate DB user | |
| let new_user = generateUserObject(user_uid, user_uid, null, 'ldap'); | |
| await db_api.insertRecordIntoTable('users', new_user); | |
| db_user = new_user; | |
| logger.verbose(`Generated new user ${user_uid} using LDAP`); | |
| } | |
| return done(null, db_user); | |
| } | |
| )); | |
| /********************************** | |
| * 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 = async function(req, res) { | |
| res.status(200).json({ | |
| user: req.user, | |
| token: req.token, | |
| permissions: await 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 = (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 (user_uid, new_pass) => { | |
| try { | |
| const hash = await bcrypt.hash(new_pass, saltRounds); | |
| await db_api.updateRecord('users', {uid: user_uid}, {passhash: hash}); | |
| return true; | |
| } catch (err) { | |
| return false; | |
| } | |
| } | |
| // change user permissions | |
| exports.changeUserPermissions = async (user_uid, permission, new_value) => { | |
| try { | |
| await db_api.pullFromRecordsArray('users', {uid: user_uid}, 'permissions', permission); | |
| await db_api.pullFromRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission); | |
| if (new_value === 'yes') { | |
| await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permissions', permission); | |
| await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission); | |
| } else if (new_value === 'no') { | |
| await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission); | |
| } | |
| return true; | |
| } catch (err) { | |
| logger.error(err); | |
| return false; | |
| } | |
| } | |
| // change role permissions | |
| exports.changeRolePermissions = async (role, permission, new_value) => { | |
| try { | |
| await db_api.pullFromRecordsArray('roles', {key: role}, 'permissions', permission); | |
| if (new_value === 'yes') { | |
| await db_api.pushToRecordsArray('roles', {key: role}, 'permissions', permission); | |
| } | |
| return true; | |
| } catch (err) { | |
| logger.error(err); | |
| return false; | |
| } | |
| } | |
| exports.adminExists = async function() { | |
| return !!(await db_api.getRecord('users', {uid: 'admin'})); | |
| } | |
| // video stuff | |
| exports.getUserVideos = async function(user_uid, type) { | |
| const files = await db_api.getRecords('files', {user_uid: user_uid}); | |
| return type ? files.filter(file => file.isAudio === (type === 'audio')) : files; | |
| } | |
| exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) { | |
| let file = await db_api.getRecord('files', {uid: file_uid}); | |
| // prevent unauthorized users from accessing the file info | |
| if (file && !file['sharingEnabled'] && requireSharing) file = null; | |
| return file; | |
| } | |
| exports.removePlaylist = async function(user_uid, playlistID) { | |
| await db_api.removeRecord('playlist', {playlistID: playlistID}); | |
| return true; | |
| } | |
| exports.getUserPlaylists = async function(user_uid) { | |
| return await db_api.getRecords('playlists', {user_uid: user_uid}); | |
| } | |
| exports.getUserPlaylist = async function(user_uid, playlistID, requireSharing = false) { | |
| let playlist = await db_api.getRecord('playlists', {id: playlistID}); | |
| // prevent unauthorized users from accessing the file info | |
| if (requireSharing && !playlist['sharingEnabled']) playlist = null; | |
| return playlist; | |
| } | |
| exports.changeSharingMode = async function(user_uid, file_uid, is_playlist, enabled) { | |
| let success = false; | |
| is_playlist ? await db_api.updateRecord(`playlists`, {id: file_uid}, {sharingEnabled: enabled}) : await db_api.updateRecord(`files`, {uid: file_uid}, {sharingEnabled: enabled}); | |
| success = true; | |
| return success; | |
| } | |
| exports.userHasPermission = async function(user_uid, permission) { | |
| const user_obj = await db_api.getRecord('users', ({uid: user_uid})); | |
| const role = user_obj['role']; | |
| if (!role) { | |
| // role doesn't exist | |
| logger.error('Invalid role ' + role); | |
| return false; | |
| } | |
| 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 | |
| const role_has_permission = await exports.roleHasPermissions(role, permission); | |
| if (role_has_permission) { | |
| return true; | |
| } else { | |
| logger.verbose(`User ${user_uid} failed to get permission ${permission}`); | |
| return false; | |
| } | |
| } | |
| exports.roleHasPermissions = async function(role, permission) { | |
| const role_obj = await db_api.getRecord('roles', {key: role}) | |
| if (!role) { | |
| logger.error(`Role ${role} does not exist!`); | |
| } | |
| const role_permissions = role_obj['permissions']; | |
| if (role_permissions && role_permissions.includes(permission)) return true; | |
| else return false; | |
| } | |
| exports.userPermissions = async function(user_uid) { | |
| let user_permissions = []; | |
| const user_obj = await db_api.getRecord('users', ({uid: user_uid})); | |
| const role = user_obj['role']; | |
| if (!role) { | |
| // role doesn't exist | |
| logger.error('Invalid role ' + role); | |
| return null; | |
| } | |
| const role_obj = await db_api.getRecord('roles', {key: role}); | |
| const role_permissions = role_obj['permissions']; | |
| 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; | |
| } | |
| }; | |
| function generateUserObject(userid, username, hash, auth_method = 'internal') { | |
| let new_user = { | |
| name: username, | |
| uid: userid, | |
| passhash: auth_method === 'internal' ? hash : null, | |
| files: [], | |
| playlists: [], | |
| subscriptions: [], | |
| created: Date.now(), | |
| role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user', | |
| permissions: [], | |
| permission_overrides: [], | |
| auth_method: auth_method | |
| }; | |
| return new_user; | |
| } | |