Source: auth/serviceAuth.js

'use strict'

const { isEmpty } = require('lodash')
const modLog = require('an-log')('$auth')

const displayError = require('../ressource/displayError')
/**
 * Service d'authentification, qui sert de proxy vers les différents authClient enregistrés
 * @service $auth
 */
module.exports = function (component) {
  component.service('$auth', function ($accessControl) {
    /**
     * Vérifie qu'un service d'authentification est conforme pour pouvoir l'ajouter
     * @private
     * @param {AuthClient} authClient
     */
    function checkValidClient (authClient) {
      const msg = 'Service d’authentification invalide, il manque '
      if (typeof authClient.baseId !== 'string') throw new Error(msg + 'baseId')
      if (clients[authClient.baseId]) throw new Error(`Le client ${authClient.baseId} est déjà enregistré`)
      if (typeof authClient.description !== 'string') throw new Error(msg + 'description')
      if (typeof authClient.login !== 'function') throw new Error(msg + 'login')
      if (typeof authClient.logout !== 'function') throw new Error(msg + 'logout')
    }

    /**
     * Retourne le authClient pour ce contexte, ou une erreur si on l'a pas trouvé
     * @private
     * @param {Context} context
     * @returns {AuthClient}
     * @throws {Error} si on a pas de authBaseId
     */
    function getClient (context) {
      const origine = getBaseId(context)
      return clients[origine]
    }

    /**
     * Retourne le authBaseId pour ce contexte, ou lance une erreur si on l'a pas trouvé
     * @private
     * @param {Context} context
     * @returns {string}
     * @throws {Error} si on a pas trouvé de authBaseId
     */
    function getBaseId (context) {
      const baseId = context.session.authBaseId || context.get.origine || context.post.origine
      if (!baseId) throw new Error('Aucun client d’authentification n’a été enregistré')
      if (!clients[baseId]) throw new Error(`Aucun client d’authentification sur ${baseId} n’a été enregistré`)

      return baseId
    }

    /**
     * Retourne l'url de retour (pour revenir ici après une connexion ou déconnexion)
     * @private
     * @param {Context} context
     * @return {string}
     */
    function getUrlRetour (context) {
      // si on est sur /connexion ou /deconnexion faudra revenir sur la home après (dé)connexion,
      // sinon la page courante
      const currentUrl = context.request.originalUrl
      // sauf si on est sur l'api, dans ce cas on renvoie ((s))
      if (/\/api\//.test(currentUrl)) return '((s))'
      return currentUrl.replace(/\/(:?de)?connexion/, '')
    }

    /**
     * La liste des clients d'authentification inscrits
     * @private
     */
    const clients = {}

    /**
     * Le controleur en attente de client
     * @private
     */
    let deferredInitController

    /**
     * Inscrit un client d'authentification
     * Chaque service d'authentification devra appeler cette méthode pour s'inscrire en passant un objet AuthClient
     * @param {AuthClient} authClient
     * @memberOf $auth
     */
    function addClient (authClient) {
      try {
        checkValidClient(authClient)
        if (isEmpty(clients)) {
          // faut activer le controleur
          if (deferredInitController) deferredInitController()
        }
        clients[authClient.baseId] = authClient
        modLog('has registered', `authClient ${authClient.baseId}`)
      } catch (error) {
        log.error(error, authClient)
      }
    }

    /**
     * Lance  initController() si un client est déjà enregistré ou le garde en attente pour le lancer au premier client qui s'enregistrera
     * @memberOf $auth
     * @param {function} initController
     */
    function deferController (initController) {
      modLog('adding', 'controller')
      if (isEmpty(clients)) deferredInitController = initController
      else initController()
    }

    /**
     * Retourne les infos pour le bloc d'authentification
     * @memberOf $auth
     * @param {Context} context
     * @returns {object} authBloc, avec les propriétés user, ssoLinks, loginLink, loginLinks, logoutLink
     */
    function getAuthBloc (context) {
      const authBloc = {}
      if ($accessControl.isAuthenticated(context)) {
        // menu authentifié
        const { pid, nom, prenom } = $accessControl.getCurrentUser(context)
        authBloc.user = { pid, nom, prenom }
        // éventuels liens spécifiques au sso
        authBloc.ssoLinks = getSsoLinks(context)
        // lien de logout
        authBloc.logoutLink = {
          href: getLogoutUrl(context),
          icon: 'sign-out-alt',
          value: 'Déconnexion'
        }
      } else {
        const loginLinks = getLoginLinks(context)
        if (loginLinks.length > 1) {
          // y'en a plusieurs, un bouton pour ouvrir le menu
          authBloc.loginLink = {
            href: '#',
            icon: 'sign-in-alt',
            value: 'Connexion'
          }
          // et les liens
          authBloc.loginLinks = loginLinks
        } else if (loginLinks.length === 1) {
          // y'en a qu'un, un seul bouton
          authBloc.loginLink = {
            href: loginLinks[0].href,
            icon: 'sign-in-alt',
            value: 'Connexion'
          }
        }
        // sinon, aucun lien, authBloc reste vide et la vue ne sera pas rendue
      }

      return authBloc
    }

    /**
     * Retourne la liste des urls de login possible (une par SSO enregistré)
     * @memberOf $auth
     * @param {Context} context
     */
    function getLoginLinks (context) {
      const urlRetour = getUrlRetour(context)
      // lien(s) de connexion
      const loginLinks = []
      Object.keys(clients).forEach(baseId => {
        const client = clients[baseId]
        const url = client.getLoginUrl && client.getLoginUrl(context, urlRetour)
        if (url) {
          const link = {
            href: url,
            icon: 'arrow-right',
            value: client.description
          }
          if (!link.value) {
            log.error(`client ${client.baseId} sans description`, client)
            link.value = `login sur ${client.baseId}`
          }
          loginLinks.push(link)
          // pour le moment labomep2 ne gère pas ça
          // } else {
          //   log.error(`client ${client.baseId} sans getLoginUrl`, client)
        }
      })

      return loginLinks
    }

    /**
     * Retourne le lien de logout
     * @param context
     * @return {string}
     */
    function getLogoutUrl (context) {
      if (!$accessControl.isAuthenticated(context)) return
      const urlRetour = getUrlRetour(context)
      let url
      try {
        const client = getClient(context)
        url = client && client.getLogoutUrl && client.getLogoutUrl(context, urlRetour)
      } catch (error) {
        log.error(error)
      }
      if (!url) url = `/deconnexion?redirect=${encodeURIComponent(urlRetour)}`

      return url
    }

    /**
     * Retourne le nom du client (pour affichage à l'utilisateur)
     * @param context
     * @return {AuthClient|String}
     */
    function getName (context) {
      const client = getClient(context)
      return client && client.name
    }

    /**
     * Renvoie les liens à mettre dans le panneau authentifié d'une personne loggée
     * @memberOf $auth
     * @param {Context} context
     * @returns {Link[]} La liste de liens
     */
    function getSsoLinks (context) {
      let links = []
      const personne = $accessControl.getCurrentUser(context)
      if (personne && personne.pid) {
        try {
          const client = getClient(context)
          if (client.getSsoLinks) {
            const slashPos = personne.pid.indexOf('/')
            const authBaseId = slashPos > 0 && personne.pid.substr(slashPos + 1)
            links = client.getSsoLinks(authBaseId)
          } else {
            log.error(`client ${client.baseId} sans getSsoLinks`)
          }
        } catch (error) {
          log.error(error)
        }
      }

      return links
    }

    /**
     * Redirige vers la connexion du serveur d'authentification
     * ou affiche une erreur
     * @memberOf $auth
     * @param {Context} context
     */
    function login (context) {
      if ($accessControl.isAuthenticated(context)) {
        if (context.get.redirect) context.redirect(context.get.redirect)
        else displayError(context, 'Utilisateur déjà connecté')
      } else {
        const client = getClient(context)
        if (client instanceof Error) {
          displayError(context, client)
        } else {
          client.login(context)
        }
      }
    }

    /**
     * Déconnecte localement puis redirige vers la déconnexion du serveur d'authentification (qui rappellera logoutFromRemote)
     * ou affiche une erreur
     * @memberOf $auth
     * @param {Context} context
     */
    function logout (context) {
      if ($accessControl.isAuthenticated(context)) {
        $accessControl.logout(context)
        const client = getClient(context)
        if (client instanceof Error) displayError(context, client)
        else client.logout(context)
      } else {
        log.debug('Pas de user en session', context.session)
        displayError(context, 'Pas d’utilisateur connecté (donc personne à déconnecter)')
      }
    }

    return {
      addClient,
      deferController,
      getAuthBloc,
      getLogoutUrl,
      getLoginLinks,
      getName,
      getSsoLinks,
      login,
      logout
    }
  })
}