diff --git a/config.sample.yml b/config.sample.yml index 47edd8d28f..579a64495e 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -31,6 +31,28 @@ db: db: wiki ssl: false + # Optional - How often (in ms) to clean up expired sessions from the DB (default: 86400000 = 1 day) + # sessionCleanupInterval: 86400000 + + # Optional - How often (in ms) the /healthz endpoint checks the DB (default: 14400000 = 4 hours) + # healthCheckInterval: 14400000 + +# --------------------------------------------------------------------- +# Analytics +# --------------------------------------------------------------------- + +# analytics: + # Optional - How long (in seconds) to cache analytics config (default: 86400 = 1 day) + # cacheTTL: 86400 + +# --------------------------------------------------------------------- +# Authentication +# --------------------------------------------------------------------- + +# auth: + # Optional - How long (in seconds) to cache deserialized users (default: 14400 = 4 hours, 0 to disable) + # userCacheTTL: 14400 + # Optional - PostgreSQL / MySQL / MariaDB only: # -> Uncomment lines you need below and set `auto` to false # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options diff --git a/dev/build/config.yml b/dev/build/config.yml index d9e58761a1..8533806faa 100644 --- a/dev/build/config.yml +++ b/dev/build/config.yml @@ -9,6 +9,10 @@ db: db: $(DB_NAME) storage: $(DB_FILEPATH) ssl: $(DB_SSL) + sessionCleanupInterval: $(DB_SESSION_CLEANUP_INTERVAL:86400000) + healthCheckInterval: $(DB_HEALTH_CHECK_INTERVAL:14400000) +pool: + min: $(DB_POOL_MIN:2) ssl: enabled: $(SSL_ACTIVE) port: 3443 @@ -18,3 +22,7 @@ ssl: logLevel: $(LOG_LEVEL:info) logFormat: $(LOG_FORMAT:default) ha: $(HA_ACTIVE) +analytics: + cacheTTL: $(ANALYTICS_CACHE_TTL:86400) +auth: + userCacheTTL: $(AUTH_USER_CACHE_TTL:14400) diff --git a/server/app/data.yml b/server/app/data.yml index 0cd628a6c9..cfc9ee186d 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -18,10 +18,12 @@ defaults: storage: ./db.sqlite sslOptions: auto: true + sessionCleanupInterval: 86400000 + healthCheckInterval: 14400000 ssl: enabled: false pool: - min: 1 + min: 2 bindIP: 0.0.0.0 logLevel: info logFormat: default @@ -61,6 +63,8 @@ defaults: iconset: 'md' darkMode: false tocPosition: 'left' + analytics: + cacheTTL: 86400 auth: autoLogin: false enforce2FA: false @@ -69,6 +73,7 @@ defaults: audience: 'urn:wiki.js' tokenExpiration: '30m' tokenRenewal: '14d' + userCacheTTL: 14400 editShortcuts: editFab: true editMenuBar: false diff --git a/server/controllers/common.js b/server/controllers/common.js index dec6fa3aa7..12811ee54c 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -22,16 +22,6 @@ router.get('/robots.txt', (req, res, next) => { } }) -/** - * Health Endpoint - */ -router.get('/healthz', (req, res, next) => { - if (WIKI.models.knex.client.pool.numFree() < 1 && WIKI.models.knex.client.pool.numUsed() < 1) { - res.status(503).json({ ok: false }).end() - } else { - res.status(200).json({ ok: true }).end() - } -}) /** * Administration diff --git a/server/core/auth.js b/server/core/auth.js index fb30c970c2..68cb6e44b7 100644 --- a/server/core/auth.js +++ b/server/core/auth.js @@ -21,6 +21,7 @@ module.exports = { groups: {}, validApiKeys: [], revocationList: require('./cache').init(), + userCache: require('./cache').init(), /** * Initialize the authentication module @@ -34,10 +35,20 @@ module.exports = { passport.deserializeUser(async (id, done) => { try { + const ttl = WIKI.config.auth.userCacheTTL + if (ttl > 0) { + const cached = this.userCache.get(id) + if (cached !== undefined) { + return done(null, cached) + } + } const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => { builder.select('groups.id', 'permissions') }) if (user) { + if (ttl > 0) { + this.userCache.set(id, user, ttl) + } done(null, user) } else { done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null) @@ -351,6 +362,9 @@ module.exports = { const groupsArray = await WIKI.models.groups.query() this.groups = _.keyBy(groupsArray, 'id') WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 }) + // Cached deserialized users embed group permissions — drop them so + // group/permission changes take effect immediately, not after userCacheTTL. + this.userCache.flushAll() }, /** diff --git a/server/graph/resolvers/user.js b/server/graph/resolvers/user.js index a94aef8a5a..e139afd1b1 100644 --- a/server/graph/resolvers/user.js +++ b/server/graph/resolvers/user.js @@ -135,6 +135,7 @@ module.exports = { await WIKI.models.users.query().patch({ isActive: false }).findById(args.id) WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' }) + WIKI.auth.userCache.del(args.id) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' }) return { diff --git a/server/master.js b/server/master.js index 0679777ef8..483c9ba2f3 100644 --- a/server/master.js +++ b/server/master.js @@ -71,6 +71,31 @@ module.exports = async () => { app.use('/', ctrl.ssl) + // ---------------------------------------- + // Health Endpoint (before Passport to avoid DB queries on healthcheck) + // ---------------------------------------- + + // A success is cached for the full interval (keeps the DB idle), but a failure + // is only cached briefly — /healthz is the ALB + ECS health check, and pinning + // one transient DB blip as 503 for the whole interval would cycle healthy tasks. + const HEALTH_FAIL_RECHECK_MS = 60 * 1000 + let healthLastCheck = 0 + let healthLastOk = false + app.get('/healthz', async (req, res) => { + const now = Date.now() + const maxAge = healthLastOk ? WIKI.config.db.healthCheckInterval : HEALTH_FAIL_RECHECK_MS + if (now - healthLastCheck > maxAge) { + try { + await WIKI.models.knex.raw('SELECT 1') + healthLastOk = true + } catch (err) { + healthLastOk = false + } + healthLastCheck = now + } + res.status(healthLastOk ? 200 : 503).json({ ok: healthLastOk }).end() + }) + // ---------------------------------------- // Passport Authentication // ---------------------------------------- @@ -81,7 +106,8 @@ module.exports = async () => { resave: false, saveUninitialized: false, store: new KnexSessionStore({ - knex: WIKI.models.knex + knex: WIKI.models.knex, + clearInterval: WIKI.config.db.sessionCleanupInterval }) })) app.use(WIKI.auth.passport.initialize()) diff --git a/server/models/analytics.js b/server/models/analytics.js index 17a3f6ab54..6895d7a407 100644 --- a/server/models/analytics.js +++ b/server/models/analytics.js @@ -127,7 +127,7 @@ module.exports = class Analytics extends Model { analyticsCode.bodyEnd += code.bodyEnd } - await WIKI.cache.set('analytics', analyticsCode, 300) + await WIKI.cache.set('analytics', analyticsCode, WIKI.config.analytics.cacheTTL) return analyticsCode } catch (err) { diff --git a/server/models/users.js b/server/models/users.js index 8996206d63..ec08e89481 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -731,6 +731,8 @@ module.exports = class User extends Model { usrData.appearance = appearance } await WIKI.models.users.query().patch(usrData).findById(id) + // Drop the deserialize cache so group/permission changes apply immediately + WIKI.auth.userCache.del(id) } else { throw new WIKI.Error.UserNotFound() } @@ -752,6 +754,7 @@ module.exports = class User extends Model { await WIKI.models.userKeys.query().delete().where('userId', id) await WIKI.models.users.query().deleteById(id) + WIKI.auth.userCache.del(id) } else { throw new WIKI.Error.UserNotFound() }