mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			206 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const bcrypt = require('bcryptjs')
 | 
						|
const jwt = require('jsonwebtoken')
 | 
						|
const Logger = require('./Logger')
 | 
						|
 | 
						|
class Auth {
 | 
						|
  constructor(db) {
 | 
						|
    this.db = db
 | 
						|
 | 
						|
    this.user = null
 | 
						|
  }
 | 
						|
 | 
						|
  get username() {
 | 
						|
    return this.user ? this.user.username : 'nobody'
 | 
						|
  }
 | 
						|
 | 
						|
  get users() {
 | 
						|
    return this.db.users
 | 
						|
  }
 | 
						|
 | 
						|
  init() {
 | 
						|
    var root = this.users.find(u => u.type === 'root')
 | 
						|
    if (!root) {
 | 
						|
      Logger.fatal('No Root User', this.users)
 | 
						|
      throw new Error('No Root User')
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  cors(req, res, next) {
 | 
						|
    res.header('Access-Control-Allow-Origin', '*')
 | 
						|
    res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
 | 
						|
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization")
 | 
						|
    res.header('Access-Control-Allow-Credentials', true)
 | 
						|
    if (req.method === 'OPTIONS') {
 | 
						|
      res.sendStatus(200)
 | 
						|
    } else {
 | 
						|
      next()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async authMiddleware(req, res, next) {
 | 
						|
    var token = null
 | 
						|
 | 
						|
    // If using a get request, the token can be passed as a query string
 | 
						|
    if (req.method === 'GET' && req.query && req.query.token) {
 | 
						|
      token = req.query.token
 | 
						|
    } else {
 | 
						|
      const authHeader = req.headers['authorization']
 | 
						|
      token = authHeader && authHeader.split(' ')[1]
 | 
						|
    }
 | 
						|
 | 
						|
    if (token == null) {
 | 
						|
      Logger.error('Api called without a token', req.path)
 | 
						|
      return res.sendStatus(401)
 | 
						|
    }
 | 
						|
 | 
						|
    var user = await this.verifyToken(token)
 | 
						|
    if (!user) {
 | 
						|
      Logger.error('Verify Token User Not Found', token)
 | 
						|
      return res.sendStatus(404)
 | 
						|
    }
 | 
						|
    if (!user.isActive) {
 | 
						|
      Logger.error('Verify Token User is disabled', token, user.username)
 | 
						|
      return res.sendStatus(403)
 | 
						|
    }
 | 
						|
    req.user = user
 | 
						|
    next()
 | 
						|
  }
 | 
						|
 | 
						|
  hashPass(password) {
 | 
						|
    return new Promise((resolve) => {
 | 
						|
      bcrypt.hash(password, 8, (err, hash) => {
 | 
						|
        if (err) {
 | 
						|
          Logger.error('Hash failed', err)
 | 
						|
          resolve(null)
 | 
						|
        } else {
 | 
						|
          resolve(hash)
 | 
						|
        }
 | 
						|
      })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  generateAccessToken(payload) {
 | 
						|
    return jwt.sign(payload, process.env.TOKEN_SECRET);
 | 
						|
  }
 | 
						|
 | 
						|
  authenticateUser(token) {
 | 
						|
    return this.verifyToken(token)
 | 
						|
  }
 | 
						|
 | 
						|
  verifyToken(token) {
 | 
						|
    return new Promise((resolve) => {
 | 
						|
      jwt.verify(token, process.env.TOKEN_SECRET, (err, payload) => {
 | 
						|
        if (!payload || err) {
 | 
						|
          Logger.error('JWT Verify Token Failed', err)
 | 
						|
          return resolve(null)
 | 
						|
        }
 | 
						|
        var user = this.users.find(u => u.id === payload.userId)
 | 
						|
        resolve(user || null)
 | 
						|
      })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getUserLoginResponsePayload(user) {
 | 
						|
    return {
 | 
						|
      user: user.toJSONForBrowser(),
 | 
						|
      userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
 | 
						|
      serverSettings: this.db.serverSettings.toJSON()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async login(req, res) {
 | 
						|
    var username = (req.body.username || '').toLowerCase()
 | 
						|
    var password = req.body.password || ''
 | 
						|
    Logger.debug('Check Auth', username, !!password)
 | 
						|
 | 
						|
    var user = this.users.find(u => u.username.toLowerCase() === username)
 | 
						|
 | 
						|
    if (!user || !user.isActive) {
 | 
						|
      Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
 | 
						|
      if (req.rateLimit.remaining <= 2) {
 | 
						|
        return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
 | 
						|
      }
 | 
						|
      return res.status(401).send('Invalid user or password')
 | 
						|
    }
 | 
						|
 | 
						|
    // Check passwordless root user
 | 
						|
    if (user.id === 'root' && (!user.pash || user.pash === '')) {
 | 
						|
      if (password) {
 | 
						|
        return res.status(401).send('Invalid root password (hint: there is none)')
 | 
						|
      } else {
 | 
						|
        return res.json(this.getUserLoginResponsePayload(user))
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Check password match
 | 
						|
    var compare = await bcrypt.compare(password, user.pash)
 | 
						|
    if (compare) {
 | 
						|
      res.json(this.getUserLoginResponsePayload(user))
 | 
						|
    } else {
 | 
						|
      Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
 | 
						|
      if (req.rateLimit.remaining <= 2) {
 | 
						|
        Logger.error(`[Auth] Failed login attempt for user ${user.username}. Attempts: ${req.rateLimit.current}`)
 | 
						|
        return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
 | 
						|
      }
 | 
						|
      return res.status(401).send('Invalid user or password')
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Not in use now
 | 
						|
  lockUser(user) {
 | 
						|
    user.isLocked = true
 | 
						|
    return this.db.updateEntity('user', user).catch((error) => {
 | 
						|
      Logger.error('[Auth] Failed to lock user', user.username, error)
 | 
						|
      return false
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  comparePassword(password, user) {
 | 
						|
    if (user.type === 'root' && !password && !user.pash) return true
 | 
						|
    if (!password || !user.pash) return false
 | 
						|
    return bcrypt.compare(password, user.pash)
 | 
						|
  }
 | 
						|
 | 
						|
  async userChangePassword(req, res) {
 | 
						|
    var { password, newPassword } = req.body
 | 
						|
    newPassword = newPassword || ''
 | 
						|
    var matchingUser = this.users.find(u => u.id === req.user.id)
 | 
						|
 | 
						|
    // Only root can have an empty password
 | 
						|
    if (matchingUser.type !== 'root' && !newPassword) {
 | 
						|
      return res.json({
 | 
						|
        error: 'Invalid new password - Only root can have an empty password'
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    var compare = await this.comparePassword(password, matchingUser)
 | 
						|
    if (!compare) {
 | 
						|
      return res.json({
 | 
						|
        error: 'Invalid password'
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    var pw = ''
 | 
						|
    if (newPassword) {
 | 
						|
      pw = await this.hashPass(newPassword)
 | 
						|
      if (!pw) {
 | 
						|
        return res.json({
 | 
						|
          error: 'Hash failed'
 | 
						|
        })
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    matchingUser.pash = pw
 | 
						|
    var success = await this.db.updateEntity('user', matchingUser)
 | 
						|
    if (success) {
 | 
						|
      res.json({
 | 
						|
        success: true
 | 
						|
      })
 | 
						|
    } else {
 | 
						|
      res.json({
 | 
						|
        error: 'Unknown error'
 | 
						|
      })
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
module.exports = Auth |