mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			187 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const passport = require('passport')
 | 
						|
const LocalStrategy = require('../libs/passportLocal')
 | 
						|
const Database = require('../Database')
 | 
						|
const Logger = require('../Logger')
 | 
						|
 | 
						|
const bcrypt = require('../libs/bcryptjs')
 | 
						|
const requestIp = require('../libs/requestIp')
 | 
						|
 | 
						|
/**
 | 
						|
 * Local authentication strategy using username/password
 | 
						|
 */
 | 
						|
class LocalAuthStrategy {
 | 
						|
  constructor() {
 | 
						|
    this.name = 'local'
 | 
						|
    this.strategy = null
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the passport strategy instance
 | 
						|
   * @returns {LocalStrategy}
 | 
						|
   */
 | 
						|
  getStrategy() {
 | 
						|
    if (!this.strategy) {
 | 
						|
      this.strategy = new LocalStrategy({ passReqToCallback: true }, this.verifyCredentials.bind(this))
 | 
						|
    }
 | 
						|
    return this.strategy
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initialize the strategy with passport
 | 
						|
   */
 | 
						|
  init() {
 | 
						|
    passport.use(this.name, this.getStrategy())
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove the strategy from passport
 | 
						|
   */
 | 
						|
  unuse() {
 | 
						|
    passport.unuse(this.name)
 | 
						|
    this.strategy = null
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Verify user credentials
 | 
						|
   * @param {import('express').Request} req
 | 
						|
   * @param {string} username
 | 
						|
   * @param {string} password
 | 
						|
   * @param {Function} done - Passport callback
 | 
						|
   */
 | 
						|
  async verifyCredentials(req, username, password, done) {
 | 
						|
    // Load the user given it's username
 | 
						|
    const user = await Database.userModel.getUserByUsername(username.toLowerCase())
 | 
						|
 | 
						|
    if (!user?.isActive) {
 | 
						|
      if (user) {
 | 
						|
        this.logFailedLoginAttempt(req, user.username, 'User is not active')
 | 
						|
      } else {
 | 
						|
        this.logFailedLoginAttempt(req, username, 'User not found')
 | 
						|
      }
 | 
						|
      done(null, null)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // Check passwordless root user
 | 
						|
    if (user.type === 'root' && !user.pash) {
 | 
						|
      if (password) {
 | 
						|
        // deny login
 | 
						|
        this.logFailedLoginAttempt(req, user.username, 'Root user has no password set')
 | 
						|
        done(null, null)
 | 
						|
        return
 | 
						|
      }
 | 
						|
      // approve login
 | 
						|
      Logger.info(`[LocalAuth] User "${user.username}" logged in from ip ${requestIp.getClientIp(req)}`)
 | 
						|
 | 
						|
      done(null, user)
 | 
						|
      return
 | 
						|
    } else if (!user.pash) {
 | 
						|
      this.logFailedLoginAttempt(req, user.username, 'User has no password set. Might have been created with OpenID')
 | 
						|
      done(null, null)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // Check password match
 | 
						|
    const compare = await bcrypt.compare(password, user.pash)
 | 
						|
    if (compare) {
 | 
						|
      // approve login
 | 
						|
      Logger.info(`[LocalAuth] User "${user.username}" logged in from ip ${requestIp.getClientIp(req)}`)
 | 
						|
 | 
						|
      done(null, user)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // deny login
 | 
						|
    this.logFailedLoginAttempt(req, user.username, 'Invalid password')
 | 
						|
    done(null, null)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Log failed login attempts
 | 
						|
   * @param {import('express').Request} req
 | 
						|
   * @param {string} username
 | 
						|
   * @param {string} message
 | 
						|
   */
 | 
						|
  logFailedLoginAttempt(req, username, message) {
 | 
						|
    if (!req || !username || !message) return
 | 
						|
    Logger.error(`[LocalAuth] Failed login attempt for username "${username}" from ip ${requestIp.getClientIp(req)} (${message})`)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Hash a password with bcrypt
 | 
						|
   * @param {string} password
 | 
						|
   * @returns {Promise<string>} hash
 | 
						|
   */
 | 
						|
  hashPassword(password) {
 | 
						|
    return new Promise((resolve) => {
 | 
						|
      bcrypt.hash(password, 8, (err, hash) => {
 | 
						|
        if (err) {
 | 
						|
          resolve(null)
 | 
						|
        } else {
 | 
						|
          resolve(hash)
 | 
						|
        }
 | 
						|
      })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Compare password with user's hashed password
 | 
						|
   * @param {string} password
 | 
						|
   * @param {import('../models/User')} user
 | 
						|
   * @returns {Promise<boolean>}
 | 
						|
   */
 | 
						|
  comparePassword(password, user) {
 | 
						|
    if (user.type === 'root' && !password && !user.pash) return true
 | 
						|
    if (!password || !user.pash) return false
 | 
						|
    return bcrypt.compare(password, user.pash)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Change user password
 | 
						|
   * @param {import('../models/User')} user
 | 
						|
   * @param {string} password
 | 
						|
   * @param {string} newPassword
 | 
						|
   */
 | 
						|
  async changePassword(user, password, newPassword) {
 | 
						|
    // Only root can have an empty password
 | 
						|
    if (user.type !== 'root' && !newPassword) {
 | 
						|
      return {
 | 
						|
        error: 'Invalid new password - Only root can have an empty password'
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Check password match
 | 
						|
    const compare = await this.comparePassword(password, user)
 | 
						|
    if (!compare) {
 | 
						|
      return {
 | 
						|
        error: 'Invalid password'
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let pw = ''
 | 
						|
    if (newPassword) {
 | 
						|
      pw = await this.hashPassword(newPassword)
 | 
						|
      if (!pw) {
 | 
						|
        return {
 | 
						|
          error: 'Hash failed'
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      await user.update({ pash: pw })
 | 
						|
      Logger.info(`[LocalAuth] User "${user.username}" changed password`)
 | 
						|
      return {
 | 
						|
        success: true
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      Logger.error(`[LocalAuth] User "${user.username}" failed to change password`, error)
 | 
						|
      return {
 | 
						|
        error: 'Unknown error'
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = LocalAuthStrategy
 |