From eca51457b702d3a8fea010f8ae93bac8eb015ee5 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 4 Aug 2024 16:13:40 -0500 Subject: [PATCH] Update jsdocs and auto-formatting --- server/Auth.js | 370 ++++++++++++++------------- server/controllers/UserController.js | 46 ++-- server/models/User.js | 4 + 3 files changed, 229 insertions(+), 191 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 827870b0..fd397838 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -13,7 +13,6 @@ const Logger = require('./Logger') * @class Class for handling all the authentication related functionality. */ class Auth { - constructor() { // Map of openId sessions indexed by oauth2 state-variable this.openIdAuthSession = new Map() @@ -24,40 +23,52 @@ class Auth { */ async initPassportJs() { // Check if we should load the local strategy (username + password login) - if (global.ServerSettings.authActiveAuthMethods.includes("local")) { + if (global.ServerSettings.authActiveAuthMethods.includes('local')) { this.initAuthStrategyPassword() } // Check if we should load the openid strategy - if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + if (global.ServerSettings.authActiveAuthMethods.includes('openid')) { this.initAuthStrategyOpenID() } - // Load the JwtStrategy (always) -> for bearer token auth - passport.use(new JwtStrategy({ - jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), - secretOrKey: Database.serverSettings.tokenSecret - }, this.jwtAuthCheck.bind(this))) + // Load the JwtStrategy (always) -> for bearer token auth + passport.use( + new JwtStrategy( + { + jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), + secretOrKey: Database.serverSettings.tokenSecret + }, + this.jwtAuthCheck.bind(this) + ) + ) // define how to seralize a user (to be put into the session) passport.serializeUser(function (user, cb) { process.nextTick(function () { // only store id to session - return cb(null, JSON.stringify({ - id: user.id, - })) + return cb( + null, + JSON.stringify({ + id: user.id + }) + ) }) }) // define how to deseralize a user (use the ID to get it from the database) - passport.deserializeUser((function (user, cb) { - process.nextTick((async function () { - const parsedUserInfo = JSON.parse(user) - // load the user by ID that is stored in the session - const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) - return cb(null, dbUser) - }).bind(this)) - }).bind(this)) + passport.deserializeUser( + function (user, cb) { + process.nextTick( + async function () { + const parsedUserInfo = JSON.parse(user) + // load the user by ID that is stored in the session + const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) + return cb(null, dbUser) + }.bind(this) + ) + }.bind(this) + ) } /** @@ -92,43 +103,49 @@ class Auth { client_secret: global.ServerSettings.authOpenIDClientSecret, id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm }) - passport.use('openid-client', new OpenIDClient.Strategy({ - client: openIdClient, - params: { - redirect_uri: '/auth/openid/callback', - scope: 'openid profile email' - } - }, async (tokenset, userinfo, done) => { - try { - Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) + passport.use( + 'openid-client', + new OpenIDClient.Strategy( + { + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, + async (tokenset, userinfo, done) => { + try { + Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) - if (!userinfo.sub) { - throw new Error('Invalid userinfo, no sub') + if (!userinfo.sub) { + throw new Error('Invalid userinfo, no sub') + } + + if (!this.validateGroupClaim(userinfo)) { + throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) + } + + let user = await this.findOrCreateUser(userinfo) + + if (!user?.isActive) { + throw new Error('User not active or not found') + } + + await this.setUserGroup(user, userinfo) + await this.updateUserPermissions(user, userinfo) + + // We also have to save the id_token for later (used for logout) because we cannot set cookies here + user.openid_id_token = tokenset.id_token + + return done(null, user) + } catch (error) { + Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`) + + return done(null, null, 'Unauthorized') + } } - - if (!this.validateGroupClaim(userinfo)) { - throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) - } - - let user = await this.findOrCreateUser(userinfo) - - if (!user?.isActive) { - throw new Error('User not active or not found') - } - - await this.setUserGroup(user, userinfo) - await this.updateUserPermissions(user, userinfo) - - // We also have to save the id_token for later (used for logout) because we cannot set cookies here - user.openid_id_token = tokenset.id_token - - return done(null, user) - } catch (error) { - Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`) - - return done(null, null, 'Unauthorized') - } - })) + ) + ) } /** @@ -181,7 +198,6 @@ class Auth { return null } - user = await Database.userModel.getUserByUsername(username) if (user?.authOpenIDSub) { @@ -220,7 +236,8 @@ class Auth { */ validateGroupClaim(userinfo) { const groupClaimName = Database.serverSettings.authOpenIDGroupClaim - if (!groupClaimName) // Allow no group claim when configured like this + if (!groupClaimName) + // Allow no group claim when configured like this return true // If configured it must exist in userinfo @@ -232,22 +249,22 @@ class Auth { /** * Sets the user group based on group claim in userinfo. - * + * * @param {import('./objects/user/User')} user * @param {Object} userinfo */ async setUserGroup(user, userinfo) { const groupClaimName = Database.serverSettings.authOpenIDGroupClaim - if (!groupClaimName) // No group claim configured, don't set anything + if (!groupClaimName) + // No group claim configured, don't set anything return - if (!userinfo[groupClaimName]) - throw new Error(`Group claim ${groupClaimName} not found in userinfo`) + if (!userinfo[groupClaimName]) throw new Error(`Group claim ${groupClaimName} not found in userinfo`) - const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) + const groupsList = userinfo[groupClaimName].map((group) => group.toLowerCase()) const rolesInOrderOfPriority = ['admin', 'user', 'guest'] - let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) + let userType = rolesInOrderOfPriority.find((role) => groupsList.includes(role)) if (userType) { if (user.type === 'root') { // Check OpenID Group @@ -271,21 +288,20 @@ class Auth { /** * Updates user permissions based on the advanced permissions claim. - * + * * @param {import('./objects/user/User')} user * @param {Object} userinfo */ async updateUserPermissions(user, userinfo) { const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim - if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything + if (!absPermissionsClaim) + // No advanced permissions claim configured, don't set anything return - if (user.type === 'admin' || user.type === 'root') - return + if (user.type === 'admin' || user.type === 'root') return const absPermissions = userinfo[absPermissionsClaim] - if (!absPermissions) - throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) + if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) if (user.updatePermissionsFromExternalJSON(absPermissions)) { Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) @@ -295,8 +311,8 @@ class Auth { /** * Unuse strategy - * - * @param {string} name + * + * @param {string} name */ unuseAuthStrategy(name) { passport.unuse(name) @@ -304,8 +320,8 @@ class Auth { /** * Use strategy - * - * @param {string} name + * + * @param {string} name */ useAuthStrategy(name) { if (name === 'openid') { @@ -335,7 +351,7 @@ class Auth { * - 'api': Authentication for API use * - 'openid': OpenID authentication directly over web * - 'openid-mobile': OpenID authentication, but done via an mobile device - * + * * @param {import('express').Request} req * @param {import('express').Response} res * @param {string} authMethod - The authentication method, default is 'local'. @@ -365,7 +381,7 @@ class Auth { /** * Informs the client in the right mode about a successfull login and the token * (clients choise is restored from cookies). - * + * * @param {import('express').Request} req * @param {import('express').Response} res */ @@ -391,8 +407,8 @@ class Auth { /** * Creates all (express) routes required for authentication. - * - * @param {import('express').Router} router + * + * @param {import('express').Router} router */ async initAuthRoutes(router) { // Local strategy login route (takes username and password) @@ -422,7 +438,7 @@ class Auth { } // Generate a state on web flow or if no state supplied - const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state + const state = !isMobileFlow || !req.query.state ? OpenIDClient.generators.random() : req.query.state // Redirect URL for the SSO provider let redirectUri @@ -508,8 +524,7 @@ class Auth { function isValidRedirectUri(uri) { // Check if the redirect_uri is in the whitelist - return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || - (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*') + return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*') } }) @@ -545,77 +560,79 @@ class Auth { }) // openid strategy callback route (this receives the token from the configured openid login provider) - router.get('/auth/openid/callback', (req, res, next) => { - const oidcStrategy = passport._strategy('openid-client') - const sessionKey = oidcStrategy._key + router.get( + '/auth/openid/callback', + (req, res, next) => { + const oidcStrategy = passport._strategy('openid-client') + const sessionKey = oidcStrategy._key - if (!req.session[sessionKey]) { - return res.status(400).send('No session') - } - - // If the client sends us a code_verifier, we will tell passport to use this to send this in the token request - // The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request - // Crucial for API/Mobile clients - if (req.query.code_verifier) { - req.session[sessionKey].code_verifier = req.query.code_verifier - } - - function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { - Logger.error(JSON.stringify(logMessage, null, 2)) - if (response) { - // Depending on the error, it can also have a body - // We also log the request header the passport plugin sents for the URL - const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') - Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2)) + if (!req.session[sessionKey]) { + return res.status(400).send('No session') } - if (isMobile) { - return res.status(errorCode).send(errorMessage) - } else { - return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`) + // If the client sends us a code_verifier, we will tell passport to use this to send this in the token request + // The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request + // Crucial for API/Mobile clients + if (req.query.code_verifier) { + req.session[sessionKey].code_verifier = req.query.code_verifier } - } - function passportCallback(req, res, next) { - return (err, user, info) => { - const isMobile = req.session[sessionKey]?.mobile === true - if (err) { - return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response) + function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { + Logger.error(JSON.stringify(logMessage, null, 2)) + if (response) { + // Depending on the error, it can also have a body + // We also log the request header the passport plugin sents for the URL + const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') + Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2)) } - if (!user) { - // Info usually contains the error message from the SSO provider - return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response) + if (isMobile) { + return res.status(errorCode).send(errorMessage) + } else { + return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`) } + } - req.logIn(user, (loginError) => { - if (loginError) { - return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) + function passportCallback(req, res, next) { + return (err, user, info) => { + const isMobile = req.session[sessionKey]?.mobile === true + if (err) { + return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response) } - // The id_token does not provide access to the user, but is used to identify the user to the SSO provider - // instead it containts a JWT with userinfo like user email, username, etc. - // the client will get to know it anyway in the logout url according to the oauth2 spec - // so it is safe to send it to the client, but we use strict settings - res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' }) - next() - }) + if (!user) { + // Info usually contains the error message from the SSO provider + return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response) + } + + req.logIn(user, (loginError) => { + if (loginError) { + return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) + } + + // The id_token does not provide access to the user, but is used to identify the user to the SSO provider + // instead it containts a JWT with userinfo like user email, username, etc. + // the client will get to know it anyway in the logout url according to the oauth2 spec + // so it is safe to send it to the client, but we use strict settings + res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' }) + next() + }) + } } - } - - // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request - // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided - // We set it here again because the passport param can change between requests - return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next) - }, + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request + // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided + // We set it here again because the passport param can change between requests + return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next) + }, // on a successfull login: read the cookies and react like the client requested (callback or json) - this.handleLoginSuccessBasedOnCookie.bind(this)) + this.handleLoginSuccessBasedOnCookie.bind(this) + ) /** * Helper route used to auto-populate the openid URLs in config/authentication * Takes an issuer URL as a query param and requests the config data at "/.well-known/openid-configuration" - * + * * @example /auth/openid/config?issuer=http://192.168.1.66:9000/application/o/audiobookshelf/ */ router.get('/auth/openid/config', this.isAuthenticated, async (req, res) => { @@ -625,7 +642,7 @@ class Auth { } if (!req.query.issuer) { - return res.status(400).send('Invalid request. Query param \'issuer\' is required') + return res.status(400).send("Invalid request. Query param 'issuer' is required") } // Strip trailing slash @@ -641,23 +658,26 @@ class Auth { } } catch (error) { Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error) - return res.status(400).send('Invalid request. Query param \'issuer\' is invalid') + return res.status(400).send("Invalid request. Query param 'issuer' is invalid") } - axios.get(configUrl.toString()).then(({ data }) => { - res.json({ - issuer: data.issuer, - authorization_endpoint: data.authorization_endpoint, - token_endpoint: data.token_endpoint, - userinfo_endpoint: data.userinfo_endpoint, - end_session_endpoint: data.end_session_endpoint, - jwks_uri: data.jwks_uri, - id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported + axios + .get(configUrl.toString()) + .then(({ data }) => { + res.json({ + issuer: data.issuer, + authorization_endpoint: data.authorization_endpoint, + token_endpoint: data.token_endpoint, + userinfo_endpoint: data.userinfo_endpoint, + end_session_endpoint: data.end_session_endpoint, + jwks_uri: data.jwks_uri, + id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported + }) + }) + .catch((error) => { + Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) + res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) }) - }).catch((error) => { - Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) - res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) - }) }) // Logout route @@ -683,7 +703,7 @@ class Auth { let postLogoutRedirectUri = null if (authMethod === 'openid') { - const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' + const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http' 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 @@ -717,9 +737,9 @@ class Auth { /** * middleware to use in express to only allow authenticated users. - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ isAuthenticated(req, res, next) { // check if session cookie says that we are authenticated @@ -727,14 +747,14 @@ class Auth { next() } else { // try JWT to authenticate - passport.authenticate("jwt")(req, res, next) + passport.authenticate('jwt')(req, res, next) } } /** * Function to generate a jwt token for a given user - * - * @param {{ id:string, username:string }} user + * + * @param {{ id:string, username:string }} user * @returns {string} token */ generateAccessToken(user) { @@ -743,15 +763,14 @@ class Auth { /** * Function to validate a jwt token for a given user - * - * @param {string} token + * + * @param {string} token * @returns {Object} tokens data */ static validateAccessToken(token) { try { return jwt.verify(token, global.ServerSettings.tokenSecret) - } - catch (err) { + } catch (err) { return null } } @@ -760,7 +779,8 @@ class Auth { * Generate a token which is used to encrpt/protect the jwts. */ async initTokenSecret() { - if (process.env.TOKEN_SECRET) { // User can supply their own token secret + if (process.env.TOKEN_SECRET) { + // User can supply their own token secret Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') @@ -779,8 +799,8 @@ class Auth { /** * Checks if the user in the validated jwt_payload really exists and is active. - * @param {Object} jwt_payload - * @param {function} done + * @param {Object} jwt_payload + * @param {function} done */ async jwtAuthCheck(jwt_payload, done) { // load user by id from the jwt token @@ -798,9 +818,9 @@ class Auth { /** * Checks if a username and password tuple is valid and the user active. - * @param {string} username - * @param {string} password - * @param {Promise} done + * @param {string} username + * @param {string} password + * @param {Promise} done */ async localAuthCheckUserPw(username, password, done) { // Load the user given it's username @@ -841,8 +861,8 @@ class Auth { /** * Hashes a password with bcrypt. - * @param {string} password - * @returns {Promise} hash + * @param {string} password + * @returns {Promise} hash */ hashPass(password) { return new Promise((resolve) => { @@ -858,8 +878,8 @@ class Auth { /** * Return the login info payload for a user - * - * @param {Object} user + * + * @param {Object} user * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { @@ -874,9 +894,9 @@ class Auth { } /** - * - * @param {string} password - * @param {import('./models/User')} user + * + * @param {string} password + * @param {import('./models/User')} user * @returns {Promise} */ comparePassword(password, user) { @@ -887,9 +907,9 @@ class Auth { /** * User changes their password from request - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async userChangePassword(req, res) { let { password, newPassword } = req.body @@ -937,4 +957,4 @@ class Auth { } } -module.exports = Auth \ No newline at end of file +module.exports = Auth diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 72677751..e222da80 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,4 +1,4 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -7,18 +7,32 @@ const User = require('../objects/user/User') const { toNumber } = require('../utils/index') -class UserController { - constructor() { } +/** + * @typedef UserControllerRequestProps + * @property {import('../objects/user/User')} user - User that made the request + * @property {import('../objects/user/User')} [reqUser] - User for req param id + * + * @typedef {import('express').Request & UserControllerRequestProps} UserControllerRequest + * @typedef {import('express').Response} UserControllerResponse + */ +class UserController { + constructor() {} + + /** + * + * @param {UserControllerRequest} req + * @param {UserControllerResponse} res + */ async findAll(req, res) { if (!req.user.isAdminOrUp) return res.sendStatus(403) const hideRootToken = !req.user.isRoot - const includes = (req.query.include || '').split(',').map(i => i.trim()) + const includes = (req.query.include || '').split(',').map((i) => i.trim()) // Minimal toJSONForBrowser does not include mediaProgress and bookmarks const allUsers = await Database.userModel.getOldUsers() - const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true)) + const users = allUsers.map((u) => u.toJSONForBrowser(hideRootToken, true)) if (includes.includes('latestSession')) { for (const user of users) { @@ -36,9 +50,9 @@ class UserController { * GET: /api/users/:id * Get a single user toJSONForBrowser * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt` - * - * @param {import("express").Request} req - * @param {import("express").Response} res + * + * @param {UserControllerRequest} req + * @param {UserControllerResponse} res */ async findOne(req, res) { if (!req.user.isAdminOrUp) { @@ -67,7 +81,7 @@ class UserController { ] }) - const oldMediaProgresses = mediaProgresses.map(mp => { + const oldMediaProgresses = mediaProgresses.map((mp) => { const oldMediaProgress = mp.getOldMediaProgress() oldMediaProgress.displayTitle = mp.mediaItem?.title if (mp.mediaItem?.podcast) { @@ -118,9 +132,9 @@ class UserController { /** * PATCH: /api/users/:id * Update user - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {UserControllerRequest} req + * @param {UserControllerResponse} res */ async update(req, res) { const user = req.reqUser @@ -196,9 +210,9 @@ class UserController { /** * PATCH: /api/users/:id/openid-unlink - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {UserControllerRequest} req + * @param {UserControllerResponse} res */ async unlinkFromOpenID(req, res) { Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) @@ -267,4 +281,4 @@ class UserController { next() } } -module.exports = new UserController() \ No newline at end of file +module.exports = new UserController() diff --git a/server/models/User.js b/server/models/User.js index a714ca0f..7b626d5a 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -19,6 +19,8 @@ class User extends Model { this.pash /** @type {string} */ this.type + /** @type {string} */ + this.token /** @type {boolean} */ this.isActive /** @type {boolean} */ @@ -35,6 +37,8 @@ class User extends Model { this.createdAt /** @type {Date} */ this.updatedAt + /** @type {import('./MediaProgress')[]?} - Only included when extended */ + this.mediaProgresses } /**