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
+ })
+ })
+})