diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 1f934c88e3..ba4df4c303 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -64,6 +64,20 @@

+

+
+ +
+
+

{{ $strings.LabelWebRedirectURLsDescription }}

+

+ {{ webCallbackURL }} +
+ {{ mobileAppCallbackURL }} +

+
+
+
@@ -164,6 +178,27 @@ export default { value: 'username' } ] + }, + subfolderOptions() { + const options = [ + { + text: 'None', + value: '' + } + ] + if (this.$config.routerBasePath) { + options.push({ + text: this.$config.routerBasePath, + value: this.$config.routerBasePath + }) + } + return options + }, + webCallbackURL() { + return `https://${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/callback` + }, + mobileAppCallbackURL() { + return `https://${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/mobile-redirect` } }, methods: { @@ -325,7 +360,8 @@ export default { }, init() { this.newAuthSettings = { - ...this.authSettings + ...this.authSettings, + authOpenIDSubfolderForRedirectURLs: this.authSettings.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : this.authSettings.authOpenIDSubfolderForRedirectURLs } this.enableLocalAuth = this.authMethods.includes('local') this.enableOpenIDAuth = this.authMethods.includes('openid') diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 0c077ed67c..75069cd337 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -679,6 +679,8 @@ "LabelViewPlayerSettings": "View player settings", "LabelViewQueue": "View player queue", "LabelVolume": "Volume", + "LabelWebRedirectURLsDescription": "Authorize these URLs in your OAuth provider to allow redirection back to the web app after login:", + "LabelWebRedirectURLsSubfolder": "Subfolder for Redirect URLs", "LabelWeekdaysToRun": "Weekdays to run", "LabelXBooks": "{0} books", "LabelXItems": "{0} items", diff --git a/server/Auth.js b/server/Auth.js index b0046799b9..74b767f5b1 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -131,7 +131,7 @@ class Auth { { client: openIdClient, params: { - redirect_uri: '/auth/openid/callback', + redirect_uri: `${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/callback`, scope: 'openid profile email' } }, @@ -480,9 +480,9 @@ class Auth { // for the request to mobile-redirect and as such the session is not shared this.openIdAuthSession.set(state, { mobile_redirect_uri: req.query.redirect_uri }) - redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString() + redirectUri = new URL(`${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/mobile-redirect`, hostUrl).toString() } else { - redirectUri = new URL('/auth/openid/callback', hostUrl).toString() + redirectUri = new URL(`${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/callback`, hostUrl).toString() if (req.query.state) { Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`) @@ -733,7 +733,7 @@ class Auth { const host = req.get('host') // TODO: ABS does currently not support subfolders for installation // If we want to support it we need to include a config for the serverurl - postLogoutRedirectUri = `${protocol}://${host}/login` + postLogoutRedirectUri = `${protocol}://${host}${global.RouterBasePath}/login` } // else for openid-mobile we keep postLogoutRedirectUri on null // nice would be to redirect to the app here, but for example Authentik does not implement diff --git a/server/Server.js b/server/Server.js index ae9746d8d4..9153ab0921 100644 --- a/server/Server.js +++ b/server/Server.js @@ -84,7 +84,6 @@ class Server { Logger.logManager = new LogManager() this.server = null - this.io = null } /** @@ -441,18 +440,11 @@ class Server { async stop() { Logger.info('=== Stopping Server ===') Watcher.close() - Logger.info('Watcher Closed') - - return new Promise((resolve) => { - SocketAuthority.close((err) => { - if (err) { - Logger.error('Failed to close server', err) - } else { - Logger.info('Server successfully closed') - } - resolve() - }) - }) + Logger.info('[Server] Watcher Closed') + await SocketAuthority.close() + Logger.info('[Server] Closing HTTP Server') + await new Promise((resolve) => this.server.close(resolve)) + Logger.info('[Server] HTTP Server Closed') } } module.exports = Server diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index a718293617..19c686d972 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -14,7 +14,7 @@ const Auth = require('./Auth') class SocketAuthority { constructor() { this.Server = null - this.io = null + this.socketIoServers = [] /** @type {Object.} */ this.clients = {} @@ -89,82 +89,104 @@ class SocketAuthority { * * @param {Function} callback */ - close(callback) { - Logger.info('[SocketAuthority] Shutting down') - // This will close all open socket connections, and also close the underlying http server - if (this.io) this.io.close(callback) - else callback() + async close() { + Logger.info('[SocketAuthority] closing...') + const closePromises = this.socketIoServers.map((io) => { + return new Promise((resolve) => { + Logger.info(`[SocketAuthority] Closing Socket.IO server: ${io.path}`) + io.close(() => { + Logger.info(`[SocketAuthority] Socket.IO server closed: ${io.path}`) + resolve() + }) + }) + }) + await Promise.all(closePromises) + Logger.info('[SocketAuthority] closed') + this.socketIoServers = [] } initialize(Server) { this.Server = Server - this.io = new SocketIO.Server(this.Server.server, { + const socketIoOptions = { cors: { origin: '*', methods: ['GET', 'POST'] - }, - path: `${global.RouterBasePath}/socket.io` - }) - - this.io.on('connection', (socket) => { - this.clients[socket.id] = { - id: socket.id, - socket, - connected_at: Date.now() } - socket.sheepClient = this.clients[socket.id] + } - Logger.info('[SocketAuthority] Socket Connected', socket.id) + const ioServer = new SocketIO.Server(Server.server, socketIoOptions) + ioServer.path = '/socket.io' + this.socketIoServers.push(ioServer) - // Required for associating a User with a socket - socket.on('auth', (token) => this.authenticateSocket(socket, token)) + if (global.RouterBasePath) { + // open a separate socket.io server for the router base path, keeping the original server open for legacy clients + const ioBasePath = `${global.RouterBasePath}/socket.io` + const ioBasePathServer = new SocketIO.Server(Server.server, { ...socketIoOptions, path: ioBasePath }) + ioBasePathServer.path = ioBasePath + this.socketIoServers.push(ioBasePathServer) + } - // Scanning - socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId)) + this.socketIoServers.forEach((io) => { + io.on('connection', (socket) => { + this.clients[socket.id] = { + id: socket.id, + socket, + connected_at: Date.now() + } + socket.sheepClient = this.clients[socket.id] - // Logs - socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) - socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) + Logger.info(`[SocketAuthority] Socket Connected to ${io.path}`, socket.id) - // Sent automatically from socket.io clients - socket.on('disconnect', (reason) => { - Logger.removeSocketListener(socket.id) + // Required for associating a User with a socket + socket.on('auth', (token) => this.authenticateSocket(socket, token)) - const _client = this.clients[socket.id] - if (!_client) { - Logger.warn(`[SocketAuthority] Socket ${socket.id} disconnect, no client (Reason: ${reason})`) - } else if (!_client.user) { - Logger.info(`[SocketAuthority] Unauth socket ${socket.id} disconnected (Reason: ${reason})`) - delete this.clients[socket.id] - } else { - Logger.debug('[SocketAuthority] User Offline ' + _client.user.username) - this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) + // Scanning + socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId)) - const disconnectTime = Date.now() - _client.connected_at - Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`) - delete this.clients[socket.id] - } - }) + // Logs + socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) + socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) - // - // Events for testing - // - socket.on('message_all_users', (payload) => { - // admin user can send a message to all authenticated users - // displays on the web app as a toast - const client = this.clients[socket.id] || {} - if (client.user?.isAdminOrUp) { - this.emitter('admin_message', payload.message || '') - } else { - Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`) - } - }) - socket.on('ping', () => { - const client = this.clients[socket.id] || {} - const user = client.user || {} - Logger.debug(`[SocketAuthority] Received ping from socket ${user.username || 'No User'}`) - socket.emit('pong') + // Sent automatically from socket.io clients + socket.on('disconnect', (reason) => { + Logger.removeSocketListener(socket.id) + + const _client = this.clients[socket.id] + if (!_client) { + Logger.warn(`[SocketAuthority] Socket ${socket.id} disconnect, no client (Reason: ${reason})`) + } else if (!_client.user) { + Logger.info(`[SocketAuthority] Unauth socket ${socket.id} disconnected (Reason: ${reason})`) + delete this.clients[socket.id] + } else { + Logger.debug('[SocketAuthority] User Offline ' + _client.user.username) + this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) + + const disconnectTime = Date.now() - _client.connected_at + Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`) + delete this.clients[socket.id] + } + }) + + // + // Events for testing + // + socket.on('message_all_users', (payload) => { + // admin user can send a message to all authenticated users + // displays on the web app as a toast + const client = this.clients[socket.id] || {} + if (client.user?.isAdminOrUp) { + this.emitter('admin_message', payload.message || '') + } else { + Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`) + } + }) + socket.on('ping', () => { + const client = this.clients[socket.id] || {} + const user = client.user || {} + Logger.debug(`[SocketAuthority] Received ping from socket ${user.username || 'No User'}`) + socket.emit('pong') + }) }) }) } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index cf901bea03..2a87f2fef6 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -679,9 +679,9 @@ class MiscController { continue } let updatedValue = settingsUpdate[key] - if (updatedValue === '') updatedValue = null + if (updatedValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') updatedValue = null let currentValue = currentAuthenticationSettings[key] - if (currentValue === '') currentValue = null + if (currentValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') currentValue = null if (updatedValue !== currentValue) { Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 51e826000a..f46cd4ae7b 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -2,10 +2,11 @@ Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time. -| Server Version | Migration Script Name | Description | -| -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | -| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library | -| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 | -| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes | -| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model | -| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration | +| Server Version | Migration Script Name | Description | +| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library | +| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 | +| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes | +| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model | +| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration | +| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations | diff --git a/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.js b/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.js new file mode 100644 index 0000000000..03797e35e5 --- /dev/null +++ b/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.js @@ -0,0 +1,84 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +/** + * This upward migration adds an subfolder setting for OIDC redirect URIs. + * It updates existing OIDC setups to set this option to None (empty subfolder), so they continue to work as before. + * IF OIDC is not enabled, no action is taken (i.e. the subfolder is left undefined), + * so that future OIDC setups will use the default subfolder. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + // Upwards migration script + logger.info('[2.17.4 migration] UPGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris') + + const serverSettings = await getServerSettings(queryInterface, logger) + if (serverSettings.authActiveAuthMethods?.includes('openid')) { + logger.info('[2.17.4 migration] OIDC is enabled, adding authOpenIDSubfolderForRedirectURLs to server settings') + serverSettings.authOpenIDSubfolderForRedirectURLs = '' + await updateServerSettings(queryInterface, logger, serverSettings) + } else { + logger.info('[2.17.4 migration] OIDC is not enabled, no action required') + } + + logger.info('[2.17.4 migration] UPGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris') +} + +/** + * This downward migration script removes the subfolder setting for OIDC redirect URIs. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + // Downward migration script + logger.info('[2.17.4 migration] DOWNGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris ') + + // Remove the OIDC subfolder option from the server settings + const serverSettings = await getServerSettings(queryInterface, logger) + if (serverSettings.authOpenIDSubfolderForRedirectURLs !== undefined) { + logger.info('[2.17.4 migration] Removing authOpenIDSubfolderForRedirectURLs from server settings') + delete serverSettings.authOpenIDSubfolderForRedirectURLs + await updateServerSettings(queryInterface, logger, serverSettings) + } else { + logger.info('[2.17.4 migration] authOpenIDSubfolderForRedirectURLs not found in server settings, no action required') + } + + logger.info('[2.17.4 migration] DOWNGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris ') +} + +async function getServerSettings(queryInterface, logger) { + const result = await queryInterface.sequelize.query('SELECT value FROM settings WHERE key = "server-settings";') + if (!result[0].length) { + logger.error('[2.17.4 migration] Server settings not found') + throw new Error('Server settings not found') + } + + let serverSettings = null + try { + serverSettings = JSON.parse(result[0][0].value) + } catch (error) { + logger.error('[2.17.4 migration] Error parsing server settings:', error) + throw error + } + + return serverSettings +} + +async function updateServerSettings(queryInterface, logger, serverSettings) { + await queryInterface.sequelize.query('UPDATE settings SET value = :value WHERE key = "server-settings";', { + replacements: { + value: JSON.stringify(serverSettings) + } + }) +} + +module.exports = { up, down } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 8ecb8ff051..ff28027f5b 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -78,6 +78,7 @@ class ServerSettings { this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] this.authOpenIDGroupClaim = '' this.authOpenIDAdvancedPermsClaim = '' + this.authOpenIDSubfolderForRedirectURLs = undefined if (settings) { this.construct(settings) @@ -139,6 +140,7 @@ class ServerSettings { this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] this.authOpenIDGroupClaim = settings.authOpenIDGroupClaim || '' this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || '' + this.authOpenIDSubfolderForRedirectURLs = settings.authOpenIDSubfolderForRedirectURLs if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -240,7 +242,8 @@ class ServerSettings { authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client - authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim // Do not return to client + authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client + authOpenIDSubfolderForRedirectURLs: this.authOpenIDSubfolderForRedirectURLs } } @@ -286,6 +289,7 @@ class ServerSettings { authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client + authOpenIDSubfolderForRedirectURLs: this.authOpenIDSubfolderForRedirectURLs, authOpenIDSamplePermissions: User.getSampleAbsPermissions() } diff --git a/test/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.test.js b/test/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.test.js new file mode 100644 index 0000000000..1662d5f98b --- /dev/null +++ b/test/server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris.test.js @@ -0,0 +1,116 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { up, down } = require('../../../server/migrations/v2.17.4-use-subfolder-for-oidc-redirect-uris') +const { Sequelize } = require('sequelize') +const Logger = require('../../../server/Logger') + +describe('Migration v2.17.4-use-subfolder-for-oidc-redirect-uris', () => { + let queryInterface, logger, context + + beforeEach(() => { + queryInterface = { + sequelize: { + query: sinon.stub() + } + } + logger = { + info: sinon.stub(), + error: sinon.stub() + } + context = { queryInterface, logger } + }) + + describe('up', () => { + it('should add authOpenIDSubfolderForRedirectURLs if OIDC is enabled', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[{ value: JSON.stringify({ authActiveAuthMethods: ['openid'] }) }]]) + queryInterface.sequelize.query.onSecondCall().resolves() + + await up({ context }) + + expect(logger.info.calledWith('[2.17.4 migration] UPGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] OIDC is enabled, adding authOpenIDSubfolderForRedirectURLs to server settings')).to.be.true + expect(queryInterface.sequelize.query.calledTwice).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect( + queryInterface.sequelize.query.calledWith('UPDATE settings SET value = :value WHERE key = "server-settings";', { + replacements: { + value: JSON.stringify({ authActiveAuthMethods: ['openid'], authOpenIDSubfolderForRedirectURLs: '' }) + } + }) + ).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] UPGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris')).to.be.true + }) + + it('should not add authOpenIDSubfolderForRedirectURLs if OIDC is not enabled', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[{ value: JSON.stringify({ authActiveAuthMethods: [] }) }]]) + + await up({ context }) + + expect(logger.info.calledWith('[2.17.4 migration] UPGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] OIDC is not enabled, no action required')).to.be.true + expect(queryInterface.sequelize.query.calledOnce).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] UPGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris')).to.be.true + }) + + it('should throw an error if server settings cannot be parsed', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[{ value: 'invalid json' }]]) + + try { + await up({ context }) + } catch (error) { + expect(queryInterface.sequelize.query.calledOnce).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect(logger.error.calledWith('[2.17.4 migration] Error parsing server settings:')).to.be.true + expect(error).to.be.instanceOf(Error) + } + }) + + it('should throw an error if server settings are not found', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[]]) + + try { + await up({ context }) + } catch (error) { + expect(queryInterface.sequelize.query.calledOnce).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect(logger.error.calledWith('[2.17.4 migration] Server settings not found')).to.be.true + expect(error).to.be.instanceOf(Error) + } + }) + }) + + describe('down', () => { + it('should remove authOpenIDSubfolderForRedirectURLs if it exists', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[{ value: JSON.stringify({ authOpenIDSubfolderForRedirectURLs: '' }) }]]) + queryInterface.sequelize.query.onSecondCall().resolves() + + await down({ context }) + + expect(logger.info.calledWith('[2.17.4 migration] DOWNGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris ')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] Removing authOpenIDSubfolderForRedirectURLs from server settings')).to.be.true + expect(queryInterface.sequelize.query.calledTwice).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect( + queryInterface.sequelize.query.calledWith('UPDATE settings SET value = :value WHERE key = "server-settings";', { + replacements: { + value: JSON.stringify({}) + } + }) + ).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] DOWNGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris ')).to.be.true + }) + + it('should not remove authOpenIDSubfolderForRedirectURLs if it does not exist', async () => { + queryInterface.sequelize.query.onFirstCall().resolves([[{ value: JSON.stringify({}) }]]) + + await down({ context }) + + expect(logger.info.calledWith('[2.17.4 migration] DOWNGRADE BEGIN: 2.17.4-use-subfolder-for-oidc-redirect-uris ')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] authOpenIDSubfolderForRedirectURLs not found in server settings, no action required')).to.be.true + expect(queryInterface.sequelize.query.calledOnce).to.be.true + expect(queryInterface.sequelize.query.calledWith('SELECT value FROM settings WHERE key = "server-settings";')).to.be.true + expect(logger.info.calledWith('[2.17.4 migration] DOWNGRADE END: 2.17.4-use-subfolder-for-oidc-redirect-uris ')).to.be.true + }) + }) +})