adapt-authoring-auth/lib/AbstractAuthModule.js

import { AbstractModule, Hook, readJson } from 'adapt-authoring-core'
import AuthToken from './AuthToken.js'
import { loadRouteConfig } from 'adapt-authoring-server'
/**
 * Abstract module to be overridden by specific auth implementations
 * @memberof auth
 * @extends {AbstractModule}
 */
class AbstractAuthModule extends AbstractModule {
  /**
   * Initialises the module
   * @return {Promise}
   */
  async init () {
    await this.setValues()
    const [auth, users] = await this.app.waitForModule('auth', 'users')
    /**
     * Cached reference to the auth module
     * @type {AuthModule}
     */
    this.auth = auth
    /**
     * Cached reference to the auth module
     * @type {UsersModule}
     */
    this.users = users
    if (!this.type) {
      throw this.app.errors.AUTH_TYPE_DEF_MISSING
    }
    /**
     * The router instance
     * @type {Router}
     */
    this.router = this.auth.router.createChildRouter(this.type)
    if (!this.routes) {
      this.routes = await this.getDefaultRoutes()
    }
    this.applyRoutes(this.routes)
    /**
     * Hook which is invoked when a new user is registered in the system
     * @type {Hook}
     */
    this.registerHook = new Hook({ mutable: true })

    this.auth.authentication.registerPlugin(this.type, this)
  }

  /**
   * Sets initial module values (set during initialisation), can be called by subclasses
   * @return {Promise}
   */
  async setValues () {
    /**
     * Identifier for the auth type
     * @type {String}
     */
    this.type = undefined
    /**
      * Custom endpoints for the auth type
      * @type {Array<Route>}
      */
    this.routes = undefined
    /**
      * Name of the schema to use when validating a user using this auth type
      * @type {String}
      */
    this.userSchema = 'user'

    const config = await loadRouteConfig(this.rootDir, this, {
      schema: 'authroutes',
      defaults: `${import.meta.dirname}/../default-routes.json`
    })
    if (config) {
      if (config.type) this.type = config.type
      this.routes = config.routes
    }
  }

  /**
   * Returns the default auth routes for this module.
   * Loads the route template from default-routes.json and resolves handler strings.
   * @return {Promise<Array>}
   */
  async getDefaultRoutes () {
    const template = await readJson(`${import.meta.dirname}/../default-routes.json`)
    return template.routes.map(r => {
      const resolved = { ...r, handlers: {} }
      for (const [method, handlerStr] of Object.entries(r.handlers)) {
        resolved.handlers[method] = this[handlerStr].bind(this)
      }
      return resolved
    })
  }

  /**
   * Adds routes to the router and applies auth permissions from route config
   * @param {Array} routes Array of route config objects with optional permissions field
   */
  applyRoutes (routes) {
    for (const route of routes) {
      this.router.addRoute(route)
      for (const [method, scopes] of Object.entries(route.permissions || {})) {
        if (scopes === null) {
          this.unsecureRoute(route.route, method)
        } else {
          this.secureRoute(route.route, method, scopes)
        }
      }
    }
  }

  /**
   * Locks a route to only users with the passed permissions scopes
   * @param {String} route The route
   * @param {String} method The HTTP method
   * @param {Array<String>} scopes Permissions scopes
   */
  secureRoute (route, method, scopes) {
    this.auth.secureRoute(`${this.router.path}${route}`, method, scopes)
  }

  /**
   * Removes auth checks from a single route {@link Auth#unsecureRoute}
   * @param {String} route The route
   * @param {String} method The HTTP method
   */
  unsecureRoute (route, method) {
    this.auth.unsecureRoute(`${this.router.path}${route}`, method)
  }

  /**
   * Registers a new user
   * @param {Object} data Data to be used for doc creation
   * @return {Promise} Resolves with the new user's data
   */
  async register (data) {
    return this.auth.authentication.registerUser(this.type, data)
  }

  /**
   * Sets the appropriate attributes to enable/disable user
   * @param {Object} user User DB document
   * @param {boolean} isEnabled Whether the user should be enabled
   * @return {Promise}
   */
  async setUserEnabled (user, isEnabled) {
    await this.users.update({ _id: user._id }, { isEnabled })
  }

  /**
   * A convenience function for accessing Authentication#disavowUser
   * @param {object} query Search query
   * @return {Promise}
   */
  async disavowUser (query) {
    return this.auth.authentication.disavowUser(query)
  }

  /**
   * Checks whether a user is allowed access to the APIs and performs any related auth type specific actions
   * @param {Object} user The user record
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @return {Promise} Resolves on success
   */
  async authenticate (user, req, res) {
    throw this.app.errors.FUNC_NOT_OVERRIDDEN.setData({ name: `${this.constructor.name}#authenticate` })
  }

  /**
   * Handles authentication requests
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async authenticateHandler (req, res, next) {
    const { email, persistSession } = req.body
    const user = await this.users.findOne({ email }, { strict: false })
    if (!user) {
      return res.sendError(this.app.errors.INVALID_LOGIN_DETAILS)
    }
    try {
      await this.authenticate(user, req, res)

      if (req.session) {
        if (persistSession !== true) req.session.cookie.maxAge = null
        else this.log('debug', 'NEW_SESSION', user._id)

        req.session.token = await AuthToken.generate(this.type, user)
      }
      res.status(204).json()
    } catch (e) {
      this.log('debug', 'FAILED_LOGIN', !user ? 'INVALID_USER' : user?._id?.toString(), this.app.lang.translate(undefined, e))
      res.sendError(e)
    }
  }

  /**
   * Handles user enable/disable requests
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async enableHandler (req, res, next) {
    try {
      const user = await this.users.findOne({ _id: req.body._id })
      const isEnable = req.url === '/enable'
      await this.setUserEnabled(user, isEnable)
      this.log('debug', isEnable ? 'USER_ENABLE' : 'USER_DISABLE', user._id, req?.auth?.user?._id?.toString())
      res.status(204).json()
    } catch (e) {
      return next(e)
    }
  }

  /**
   * Handles user registration requests
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async registerHandler (req, res, next) {
    try {
      req.apiData = req.apiData ?? { modifying: true, data: req.body }
      await this.registerHook.invoke(req)
      const user = await this.register(req.body)
      this.log('debug', 'USER_REG', user._id, req?.auth?.user?._id?.toString())
      res.json(user)
    } catch (e) {
      return next(this.app.errors.USER_REG_FAILED.setData({ error: req.translate(e) }))
    }
  }
}

export default AbstractAuthModule