Compare commits

...

6 Commits

Author SHA1 Message Date
advplyr e678fe6e2f Update sessions modal to show username & update sessions endpoints to always return username 2025-07-16 16:56:07 -05:00
advplyr 3845940245 Add warning under legacy token input on users page to use api keys instead 2025-07-16 16:43:53 -05:00
advplyr 6c63e2131c Update AllowCors to apply to every request #4497 2025-07-15 16:28:41 -05:00
advplyr e25e2b238f Merge pull request #4493 from advplyr/localize_durations
Localize elapsed duration on sessions tables
2025-07-14 17:28:58 -05:00
advplyr b553e959e2 Merge pull request #4486 from advplyr/fix_oidc_create_user
Fix OIDC auto register user #4485
2025-07-13 17:09:40 -05:00
advplyr f7b94a4b6d Fix OIDC auto register user #4485 2025-07-13 17:04:02 -05:00
9 changed files with 49 additions and 24 deletions
@@ -81,7 +81,7 @@
</div> </div>
<div class="w-full md:w-1/3"> <div class="w-full md:w-1/3">
<p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p> <p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
<p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p> <p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ username }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p> <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
<p class="mb-1">{{ playMethodName }}</p> <p class="mb-1">{{ playMethodName }}</p>
@@ -132,6 +132,9 @@ export default {
_session() { _session() {
return this.session || {} return this.session || {}
}, },
username() {
return this._session.user?.username || this._session.userId || ''
},
deviceInfo() { deviceInfo() {
return this._session.deviceInfo || {} return this._session.deviceInfo || {}
}, },
+3 -1
View File
@@ -13,8 +13,10 @@
<widgets-online-indicator :value="!!userOnline" /> <widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1> <h1 class="text-xl pl-2">{{ username }}</h1>
</div> </div>
<div v-if="legacyToken" class="flex text-xs mt-4"> <div v-if="legacyToken" class="text-xs space-y-2 mt-4">
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy /> <ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
<p class="text-warning" v-html="$strings.MessageAuthenticationLegacyTokenWarning" />
</div> </div>
<div class="w-full h-px bg-white/10 my-2" /> <div class="w-full h-px bg-white/10 my-2" />
<div class="py-2"> <div class="py-2">
+1
View File
@@ -723,6 +723,7 @@
"MessageAddToPlayerQueue": "Add to player queue", "MessageAddToPlayerQueue": "Add to player queue",
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.", "MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Legacy API tokens will be removed in the future. Use <a href=\"/config/api-keys\">API Keys</a> instead.",
"MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.", "MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.",
"MessageAuthenticationSecurityMessage": "Authentication has been improved for security. All users are required to re-login.", "MessageAuthenticationSecurityMessage": "Authentication has been improved for security. All users are required to re-login.",
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
+1 -1
View File
@@ -240,7 +240,7 @@ class Server {
* Running in development allows cors to allow testing the mobile apps in the browser * Running in development allows cors to allow testing the mobile apps in the browser
* or env variable ALLOW_CORS = '1' * or env variable ALLOW_CORS = '1'
*/ */
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) { if (global.AllowCors || Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost'] const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) { if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
res.header('Access-Control-Allow-Origin', req.get('origin')) res.header('Access-Control-Allow-Origin', req.get('origin'))
+1 -1
View File
@@ -121,7 +121,7 @@ class OidcAuthStrategy {
throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`)
} }
let user = await Database.userModel.findOrCreateUserFromOpenIdUserInfo(userinfo, this) let user = await Database.userModel.findOrCreateUserFromOpenIdUserInfo(userinfo)
if (!user?.isActive) { if (!user?.isActive) {
throw new Error('User not active or not found') throw new Error('User not active or not found')
+13 -1
View File
@@ -81,6 +81,18 @@ class TokenManager {
} }
} }
/**
* Generate a JWT token for a given user
* TODO: Old method with no expiration
* @deprecated
*
* @param {{ id:string, username:string }} user
* @returns {string}
*/
static generateAccessToken(user) {
return jwt.sign({ userId: user.id, username: user.username }, TokenManager.TokenSecret)
}
/** /**
* Function to generate a jwt token for a given user * Function to generate a jwt token for a given user
* TODO: Old method with no expiration * TODO: Old method with no expiration
@@ -90,7 +102,7 @@ class TokenManager {
* @returns {string} * @returns {string}
*/ */
generateAccessToken(user) { generateAccessToken(user) {
return jwt.sign({ userId: user.id, username: user.username }, TokenManager.TokenSecret) return TokenManager.generateAccessToken(user)
} }
/** /**
+9 -11
View File
@@ -57,26 +57,24 @@ class SessionController {
} }
let where = null let where = null
const include = [
{
model: Database.models.device
}
]
if (userId) { if (userId) {
where = { where = {
userId userId
} }
} else {
include.push({
model: Database.userModel,
attributes: ['id', 'username']
})
} }
const { rows, count } = await Database.playbackSessionModel.findAndCountAll({ const { rows, count } = await Database.playbackSessionModel.findAndCountAll({
where, where,
include, include: [
{
model: Database.deviceModel
},
{
model: Database.userModel,
attributes: ['id', 'username']
}
],
order: [[orderKey, orderDesc]], order: [[orderKey, orderDesc]],
limit: itemsPerPage, limit: itemsPerPage,
offset: itemsPerPage * page offset: itemsPerPage * page
+10 -1
View File
@@ -439,7 +439,16 @@ class UserController {
const page = toNumber(req.query.page, 0) const page = toNumber(req.query.page, 0)
const start = page * itemsPerPage const start = page * itemsPerPage
const sessions = listeningSessions.slice(start, start + itemsPerPage) // Map user to sessions to match the format of the sessions endpoint
const sessions = listeningSessions.slice(start, start + itemsPerPage).map((session) => {
return {
...session,
user: {
id: req.reqUser.id,
username: req.reqUser.username
}
}
})
const payload = { const payload = {
total: listeningSessions.length, total: listeningSessions.length,
+7 -7
View File
@@ -1,9 +1,11 @@
const uuidv4 = require('uuid').v4 const uuidv4 = require('uuid').v4
const sequelize = require('sequelize') const sequelize = require('sequelize')
const { LRUCache } = require('lru-cache')
const Logger = require('../Logger') const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority') const SocketAuthority = require('../SocketAuthority')
const { isNullOrNaN } = require('../utils') const { isNullOrNaN } = require('../utils')
const { LRUCache } = require('lru-cache') const TokenManager = require('../auth/TokenManager')
class UserCache { class UserCache {
constructor() { constructor() {
@@ -213,10 +215,9 @@ class User extends Model {
* or creates a new user if configured to do so. * or creates a new user if configured to do so.
* *
* @param {Object} userinfo * @param {Object} userinfo
* @param {import('../Auth')} auth
* @returns {Promise<User>} * @returns {Promise<User>}
*/ */
static async findOrCreateUserFromOpenIdUserInfo(userinfo, auth) { static async findOrCreateUserFromOpenIdUserInfo(userinfo) {
let user = await this.getUserByOpenIDSub(userinfo.sub) let user = await this.getUserByOpenIDSub(userinfo.sub)
// Matched by sub // Matched by sub
@@ -290,7 +291,7 @@ class User extends Model {
// If no existing user was matched, auto-register if configured // If no existing user was matched, auto-register if configured
if (global.ServerSettings.authOpenIDAutoRegister) { if (global.ServerSettings.authOpenIDAutoRegister) {
Logger.info(`[User] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) Logger.info(`[User] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
user = await this.createUserFromOpenIdUserInfo(userinfo, auth) user = await this.createUserFromOpenIdUserInfo(userinfo)
return user return user
} }
@@ -301,16 +302,15 @@ class User extends Model {
/** /**
* Create user from openid userinfo * Create user from openid userinfo
* @param {Object} userinfo * @param {Object} userinfo
* @param {import('../Auth')} auth
* @returns {Promise<User>} * @returns {Promise<User>}
*/ */
static async createUserFromOpenIdUserInfo(userinfo, auth) { static async createUserFromOpenIdUserInfo(userinfo) {
const userId = uuidv4() const userId = uuidv4()
// TODO: Ensure username is unique? // TODO: Ensure username is unique?
const username = userinfo.preferred_username || userinfo.name || userinfo.sub const username = userinfo.preferred_username || userinfo.name || userinfo.sub
const email = userinfo.email && userinfo.email_verified ? userinfo.email : null const email = userinfo.email && userinfo.email_verified ? userinfo.email : null
const token = auth.generateAccessToken({ id: userId, username }) const token = TokenManager.generateAccessToken({ id: userId, username })
const newUser = { const newUser = {
id: userId, id: userId,