'use strict'
/**
* Configuration de l'application
*/
const path = require('path')
const { URL } = require('url')
const log = require('sesajstools/utils/log')
const sjtObj = require('sesajstools/utils/object')
const sjtUrl = require('sesajstools/http/url')
const { addSesatheque, reBaseUrl } = require('sesatheque-client/dist/server/sesatheques').default
// la conf du composant ressource à part
const configRessource = require('./ressource/config')
const { version } = require('../../package')
const checkConfigSesatheques = require('./checkConfigSesatheques')
const isTestEnv = process.argv.length > 1 && process.argv[1].includes('mocha')
/**
* Retourne les éléments de list avec une baseUrl valide
* (à laquelle on a éventuellement ajouté le slash de fin)
* @param {Array} list
* @return {Array} Liste dont tous les éléments ont une baseUrl valide
*/
function filterOnBaseUrl (list) {
return list.map((item) => {
if (item && typeof item.baseUrl === 'string') {
// on ajoute un éventuel / de fin (c'est pas immutable, mais ici on s'en fout vraiment)
if (item.baseUrl.substr(-1) !== '/') item.baseUrl += '/'
if (reBaseUrl.test(item.baseUrl)) return item
}
log.error(Error('sesatheque sans baseUrl valide'), item)
return null
}).filter((item) => item)
}
if (typeof window !== 'undefined') {
// Ce fichier contient des infos sensible qu'on ne veut pas dans le code client
// Normalement un require depuis du code client devrait passer par notre loader perso qui fait le ménage
// mais on laisse ça en sécurité (oubli dans le pattern webpack pour ce config-loader)
throw new Error('config.js should never be included in browser source code!')
}
/** La racine du projet */
const root = path.resolve(__dirname, '..', '..')
const logDir = process.env.LOGS || root + '/logs'
// la conf privée pour surcharger cette conf par défaut (et ajouter les accès à la base)
const privateConfPath = [root, '_private']
if (isTestEnv) {
privateConfPath.push('test')
} else if (process.env.SESATHEQUE_CONF) {
// on peut préciser un autre fichier de conf via l'environnement
// (utile pour faire tourner plusieurs instances de l'appli)
// on vérifie ici que y'a pas de slash dedans, pour signifier une erreur et arrêter là
if (!/^[a-zA-Z0-9_-]+$/.test(process.env.SESATHEQUE_CONF)) throw new Error(`variable d’environnement SESATHEQUE_CONF invalide (${process.env.SESATHEQUE_CONF})`)
privateConfPath.push(process.env.SESATHEQUE_CONF)
} else {
privateConfPath.push('config')
}
const localConfig = require(path.join.apply(this, privateConfPath))
const toBeConfigured = 'toBeConfigured'
/**
* Config par défaut
*/
const config = {
version,
// dans localConf, sinon conf par défaut i.e. port 3000
application: {
name: 'sesatheque',
// ajouté en title
title: 'Sésathèque',
// h1 de la page d'accueil
homeTitle: 'Bienvenue sur cette Sésathèque',
// mis dans _private/config.js car dépendant de l'instance
baseId: toBeConfigured, // l'id de cette sésathèque
baseUrl: toBeConfigured,
mail: toBeConfigured,
// staging plus loin
maintenance: {
lockFile: '_private/maintenance.lock',
message: 'Application en maintenance, merci d’essayer de nouveau dans quelques instants',
staticDir: '_private/maintenance'
}
},
$entities: {
// mongo
database: {
host: 'localhost',
port: 27017,
// cf http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect
options: {
poolSize: 10,
reconnectTries: 1800 // 1/2h avec le reconnectInterval à 1000ms par défaut
}
}
},
$server: {
port: process.env.PORT || 3001
},
$rail: {
public: true,
// compression : {},
cookie: {
key: toBeConfigured
},
// on veut pas du bodyParser de lassi
// (on met les notres pour les limiter là où ils sont utiles)
noBodyParser: true,
// ça sera pour nos bodyParser
bodyParser: {
limit: '8mb' // limite d'un post (sinon 100kb par défaut)
},
session: {
// name: 'mySessName',
secret: toBeConfigured,
saveUninitialized: true,
/* cookie : {
httpOnly : false
}, /* */
resave: true
},
authentication: {}
},
// le reste est spécifique à sesatheque et ignoré par lassi
// Cf _private.example/config.js
// une liste de tokens utilisables pour appeler l'api avec des droits en écriture
apiTokens: [],
// une liste d'autres serveurs d'authentification externes, {nom, baseId, baseUrl}
authServers: [],
// des paramètres pour nos composants
components: {
auth: {
paths: {
login: 'connexion',
logout: 'deconnexion',
externalLogout: 'deconnexion/externe'
}
},
cache: {
defaultTTL: 15 * 60,
purgeDelay: 5 * 60
},
groupe: {
cacheTTL: 20 * 60
},
// Permissions (cumulatives) pour chacun des rôles
personne: {
/**
* Les permissions possibles sont
* create: créer une ressource
* createAll: créer tout type de ressources (même celles non éditables)
* read: lire une ressource (dépend de la ressource)
* update: mettre une ressource
* updateAuteurs: mettre à jour les auteurs
* updateGroupes: mettre à jour les groupes d'une ressource
* delete: effacer une ressource
* deleteVersion: effacer une version
* index: modifier le flag indexable ou les propriétés niveau / categorie / typeDocumentaire / typePedagogique
* publish: modifier le flag publie
* correction: accéder aux corrextions
* createGroupe: créer un groupe
*/
roles: {
// les droits sont dans l'absolu, mais il peut y avoir des modifications liées au contexte
// (on a toujours le droit de modifier un contenu dont on serait le seul auteur,
// pas de droits read sur les ressources privées sauf les siennes, etc.)
admin: { create: true, createAll: true, read: true, update: true, updateAuteurs: true, updateGroupes: true, delete: true, deleteVersion: true, index: true, publish: true, correction: true, createGroupe: true },
editeur: { create: true, createAll: true, read: true, update: true, updateAuteurs: true, updateGroupes: true, delete: true, deleteVersion: true, index: true, publish: true, correction: true, createGroupe: true },
indexateur: { index: true, createGroupe: true },
formateur: { create: true, read: true, createGroupe: true },
acces_correction: { correction: true },
eleve: { read: true }
},
cacheTTL: 20 * 60
},
sesalabSso: {
authServers: []
},
ressource: configRessource
},
// les différents logs
logs: {
dir: logDir,
access: 'access.log',
error: 'error.log',
dataError: 'data.error.log',
debug: 'debug.log',
// perf : 'perf.log', log les perfs si présent
// ajouter les exclusions voulues parmi ['cache', 'resssourceRepository', 'personneRepository', 'accessControl']
debugExclusions: []
},
// une liste de plugins à charger
plugins: {
internal: [],
external: []
},
// et d'éventuelles options à leur passer
pluginsOptions: {},
// une liste de domaines 'sesalab' autorisés à appeler l'api pour stocker des séries ou séquences
// sous la forme {nom, baseId, baseUrl}
// écraser cette propriété avec un tableau vide dans _private/config.js pour s'en passer
sesalabs: [],
// urls absolues des sésathèques que l'on accepte de référencer (pour les alias, par ex quand
// des sesalab connectés à plusieurs sésathèques mettent des ressources de l'une
// dans des arbres de l'autre)
// sous la forme baseId:baseUrl, ou nomQcq{id: baseId, baseUrl:laBaseHttpAbsolue, apiToken: leToken}
// inutile d'ajouter la sesatheque courante (baseId:baseUrl), elle sera automatiquement ajoutée à la liste
sesatheques: [],
// mettre true s'il y a un varnish en frontal pour purger les urls mises en cache
varnish: false
}
// on ajoute nos params locaux (accès à la base et port,
// mais aussi tout ce qui est spécifique à une installation de sesatheque)
if (localConfig) sjtObj.merge(config, localConfig)
// Le staging dépend de l'environnement d'execution
// - si on est lancé par mocha c'est toujours test
// - sinon on prend NODE_ENV
// - sinon config.application.staging
// - sinon dev
// -pre-prod- et pas preprod pour avoir le même nombre de lettre que production,
// pour préserver les source-map lors du passage en production
// (y'a un coup de sed sur les fichiers compilés par webpack, mais pas de recompil)
const knownStagings = ['production', '-pre-prod-', 'debug', 'dev', 'test']
let stagingConf = config.application.staging
if (stagingConf === 'prod') stagingConf = 'production'
if (stagingConf === 'preprod') stagingConf = '-pre-prod-'
let staging
if (isTestEnv) {
staging = 'test'
} else if (process.argv.some(arg => arg.includes('webpack-dev-server'))) {
staging = 'dev'
} else if (process.env.NODE_ENV === 'production') {
// on laisse préprod si c'est ça qui était dans localConfig
staging = stagingConf === '-pre-prod-' ? '-pre-prod-' : 'production'
} else if (knownStagings.includes(process.env.NODE_ENV)) {
staging = process.env.NODE_ENV
} else if (knownStagings.includes(stagingConf)) {
staging = stagingConf
} else {
staging = 'production'
}
config.application.staging = staging
// pour bugsnag (il faudra mettre apiKey en private sinon il sera pas instancié
if (config.bugsnag && config.bugsnag.apiKey) {
config.bugsnag.appVersion = version
config.bugsnag.releaseStage = staging
// on pourra ajouter endpoint si on veut traiter nous-même les retours
}
// si lassiLogger n'a pas été défini on l'ajoute maintenant,
// mais en utilisant logs.dir après override de _private
if (!config.lassiLogger) {
// pour an-log, si on veut récupérer les logs db
config.lassiLogger = {
$entities: {
logLevel: config.application.staging === 'production' ? 'warning' : 'debug',
renderer: { name: 'file', target: config.logs.dir + '/entities.log' }
}
}
}
/**
* À partir le là on a la conf locale, on vérifie et normalise un peu (autant signaler une erreur dès le boot)
*/
const isConfigured = (obj, path = '') => {
if (typeof obj === 'object') {
Object.entries(obj).forEach(([prop, value]) => {
const currentPath = `${path}.${prop}`
if (typeof value === 'object') return isConfigured(value, currentPath)
if (value === toBeConfigured) throw new Error(`La propriété ${currentPath} doit être configurée`)
})
}
}
isConfigured(config, 'config')
// on vérifie quand même ça aussi (au cas où ce serait une chaîne vide)
if (!config.application.baseId) throw new Error('config.application.baseId manquant')
if (!config.application.baseUrl) throw new Error('config.application.baseUrl manquant')
// on ajoute toujours un slash de fin à baseUrl
if (config.application.baseUrl.substr(-1) !== '/') config.application.baseUrl += '/'
// on garanti que sesatheques est un tableau
if (!config.sesatheques) config.sesatheques = []
if (!Array.isArray(config.sesatheques)) {
console.error(new Error('config.sesatheques doit être un Array'))
config.sesatheques = []
}
// s'il y a du contenu il doit être conforme
if (config.sesatheques.length) {
// check baseUrl valides (avec ajout slash de fin s'il manque)
config.sesatheques = filterOnBaseUrl(config.sesatheques)
const errors = checkConfigSesatheques(config.sesatheques, true)
// en cas d'erreur on throw pour arrêter le boot
if (errors.length) {
errors.forEach(log.error)
throw new Error('il y a des erreurs dans les sésathèques en configuration')
} else {
// on s'ajoute à la liste
if (!config.sesatheques.some(st => st.baseId === config.application.baseId)) {
config.sesatheques.push({ baseId: config.application.baseId, baseUrl: config.application.baseUrl })
}
// on ajoute chaque sésathèque au registrar, si elle est déjà connue :
// - avec cette baseUrl ça renvoie false mais ne gêne pas
// - avec une autre baseUrl ça throw
config.sesatheques.forEach(({ baseId, baseUrl }) => addSesatheque(baseId, baseUrl))
}
}
// faut aussi s'ajouter nous-même, au cas où sesatheque-client ne nous connaîtrait pas
addSesatheque(config.application.baseId, config.application.baseUrl)
/**
* On passe à la conf de sesalabSso, déduite du reste si on a mis des sesalabs
* (dans ce cas on est obligatoirement client sso de ces sesalab,
* ce qui n'empêcherait pas d'être client sso d'autres sesatheques qui implémenteraient un $sesalabSsoServer)
*/
if (!config.sesalabs) config.sesalabs = []
if (!Array.isArray(config.sesalabs)) {
console.error(new Error('config.sesalabs doit être un Array'))
config.sesalabs = []
}
if (config.sesalabs.length) config.sesalabs = filterOnBaseUrl(config.sesalabs)
// pour valider du CORS, les baseUrl sans slash de fin
config.sesalabsByOrigin = {}
config.sesalabs.forEach(({ baseUrl }) => {
const origin = baseUrl.substr(0, baseUrl.length - 1)
config.sesalabsByOrigin[origin] = true
})
if (!config.components) config.components = {}
// s'il y a des sesalab on génère la config du component sesalab-sso
if (config.sesalabs.length) {
if (!config.components.sesalabSso) config.components.sesalabSso = {}
const confSso = config.components.sesalabSso
const hasLocalConf = Array.isArray(confSso.authServers) && confSso.authServers.length
confSso.authServers = hasLocalConf ? filterOnBaseUrl(confSso.authServers) : []
// et on ajoute un authServer pour chaque sesalab
config.sesalabs.forEach(function (sesalab) {
const authServer = {
name: sesalab.name || sjtUrl.getDomain(sesalab.baseUrl),
baseUrl: sesalab.baseUrl,
// les urls sur ce serveur, pour demander un login
loginPage: 'sso/login',
// pour une demande de logout (de sesalab et des autres sesatheques)
logoutPage: 'sso/logout',
// pour signaler une erreur
errorPage: 'sso/error'
}
// on regarde s'il a pas déjà été défini, pour empêcher les doublons
let existingIndex
confSso.authServers.some(function (server, index) {
if (server.name === authServer.name || server.baseUrl === authServer.baseUrl) {
existingIndex = index
sjtObj.merge(authServer, server)
return true
}
return false
})
if (existingIndex) {
confSso.authServers[existingIndex] = authServer
} else {
confSso.authServers.push(authServer)
}
})
// loginCallback pour loguer un user ici, cette fonction sera appelée après un validate réussi,
// le user est envoyé par le serveur d'authentification et mis au format User
// on a pas accès au service $sesalabSsoClient ici, ça sera initialisé dans app/index.js
// en attendant le component sesalab-sso met une fct qui renverra une erreu
// confSso.loginCallback = function (context, user, next) { throw new Error('…') }
// callback de logout, vire le user en session et on appelle next
// pour le logout on peut déjà la fournir
confSso.logoutCallback = function (context, next) {
context.session.user = null
next()
}
// et on ajoute le component sesalab-sso en dépendances
const name = 'sesalab-sso'
if (!config.extraModules) config.extraModules = []
if (!config.extraModules.includes(name)) config.extraModules.push(name)
if (!config.extraDependenciesLast) config.extraDependenciesLast = []
if (!config.extraDependenciesLast.includes(name)) config.extraDependenciesLast.push(name)
}
// on indique à webpack s'il doit mettre un devServer et où
if (staging === 'dev') {
// le port utilisé par le navigateur ne doit pas changer (pour que le sso fonctionne et ne
// pas avoir à changer baseUrl), on décale le port de node et on l'indique à devServer
if (typeof config.$server.port !== 'number') config.$server.port = Number(config.$server.port)
const frontPort = config.$server.port
if (!Number.isInteger(frontPort)) throw new Error('Il faut préciser un port dans config.$server.port')
const newNodePort = frontPort + 20 // arbitraire, en test on décale de 10
const url = new URL(config.application.baseUrl)
const defaultDevServer = {
host: url.hostname, // sans le port, mais ça marche pas (ça reste localhost), faut le préciser avec --host dans l'appel pour que ce soit vraiment pris en compte
port: frontPort
}
if (url.port) url.port = newNodePort
const proxyUrl = url.toString().replace(/\/$/, '') // sans slash de fin
config.devServer = Object.assign({}, defaultDevServer, config.devServer, { proxy: { '/': proxyUrl } })
config.$server.port = newNodePort
}
module.exports = config