2023-07-05 01:14:44 +02:00
|
|
|
const uuidv4 = require("uuid").v4
|
2023-11-22 18:00:11 +01:00
|
|
|
const sequelize = require('sequelize')
|
2023-07-05 01:14:44 +02:00
|
|
|
const Logger = require('../Logger')
|
|
|
|
const oldUser = require('../objects/user/User')
|
2023-11-22 18:00:11 +01:00
|
|
|
const SocketAuthority = require('../SocketAuthority')
|
|
|
|
const { DataTypes, Model } = sequelize
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
class User extends Model {
|
|
|
|
constructor(values, options) {
|
|
|
|
super(values, options)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.id
|
|
|
|
/** @type {string} */
|
|
|
|
this.username
|
|
|
|
/** @type {string} */
|
|
|
|
this.email
|
|
|
|
/** @type {string} */
|
|
|
|
this.pash
|
|
|
|
/** @type {string} */
|
|
|
|
this.type
|
|
|
|
/** @type {boolean} */
|
|
|
|
this.isActive
|
|
|
|
/** @type {boolean} */
|
|
|
|
this.isLocked
|
|
|
|
/** @type {Date} */
|
|
|
|
this.lastSeen
|
|
|
|
/** @type {Object} */
|
|
|
|
this.permissions
|
|
|
|
/** @type {Object} */
|
|
|
|
this.bookmarks
|
|
|
|
/** @type {Object} */
|
|
|
|
this.extraData
|
|
|
|
/** @type {Date} */
|
|
|
|
this.createdAt
|
|
|
|
/** @type {Date} */
|
|
|
|
this.updatedAt
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get all oldUsers
|
|
|
|
* @returns {Promise<oldUser>}
|
|
|
|
*/
|
|
|
|
static async getOldUsers() {
|
|
|
|
const users = await this.findAll({
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
return users.map(u => this.getOldUser(u))
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
* Get old user model from new
|
|
|
|
*
|
|
|
|
* @param {Object} userExpanded
|
|
|
|
* @returns {oldUser}
|
|
|
|
*/
|
2023-08-16 23:38:48 +02:00
|
|
|
static getOldUser(userExpanded) {
|
|
|
|
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
const librariesAccessible = userExpanded.permissions?.librariesAccessible || []
|
|
|
|
const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || []
|
|
|
|
const permissions = userExpanded.permissions || {}
|
|
|
|
delete permissions.librariesAccessible
|
|
|
|
delete permissions.itemTagsSelected
|
|
|
|
|
|
|
|
return new oldUser({
|
|
|
|
id: userExpanded.id,
|
|
|
|
oldUserId: userExpanded.extraData?.oldUserId || null,
|
|
|
|
username: userExpanded.username,
|
2023-10-05 00:05:12 +02:00
|
|
|
email: userExpanded.email || null,
|
2023-08-16 23:38:48 +02:00
|
|
|
pash: userExpanded.pash,
|
|
|
|
type: userExpanded.type,
|
|
|
|
token: userExpanded.token,
|
|
|
|
mediaProgress,
|
|
|
|
seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [],
|
|
|
|
bookmarks: userExpanded.bookmarks,
|
|
|
|
isActive: userExpanded.isActive,
|
|
|
|
isLocked: userExpanded.isLocked,
|
|
|
|
lastSeen: userExpanded.lastSeen?.valueOf() || null,
|
|
|
|
createdAt: userExpanded.createdAt.valueOf(),
|
|
|
|
permissions,
|
|
|
|
librariesAccessible,
|
2023-11-22 18:00:11 +01:00
|
|
|
itemTagsSelected,
|
|
|
|
authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null
|
2023-08-16 23:38:48 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {oldUser} oldUser
|
|
|
|
* @returns {Promise<User>}
|
|
|
|
*/
|
2023-08-16 23:38:48 +02:00
|
|
|
static createFromOld(oldUser) {
|
|
|
|
const user = this.getFromOld(oldUser)
|
|
|
|
return this.create(user)
|
|
|
|
}
|
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
* Update User from old user model
|
|
|
|
*
|
|
|
|
* @param {oldUser} oldUser
|
2023-11-24 21:27:32 +01:00
|
|
|
* @param {boolean} [hooks=true] Run before / after bulk update hooks?
|
2023-11-22 18:00:11 +01:00
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
*/
|
2023-11-24 21:27:32 +01:00
|
|
|
static updateFromOld(oldUser, hooks = true) {
|
2023-08-16 23:38:48 +02:00
|
|
|
const user = this.getFromOld(oldUser)
|
|
|
|
return this.update(user, {
|
2023-11-24 21:27:32 +01:00
|
|
|
hooks: !!hooks,
|
2023-08-16 23:38:48 +02:00
|
|
|
where: {
|
|
|
|
id: user.id
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
}).then((result) => result[0] > 0).catch((error) => {
|
|
|
|
Logger.error(`[User] Failed to save user ${oldUser.id}`, error)
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
* Get new User model from old
|
|
|
|
*
|
|
|
|
* @param {oldUser} oldUser
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
2023-08-16 23:38:48 +02:00
|
|
|
static getFromOld(oldUser) {
|
2023-11-22 18:00:11 +01:00
|
|
|
const extraData = {
|
|
|
|
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
|
|
|
oldUserId: oldUser.oldUserId
|
|
|
|
}
|
|
|
|
if (oldUser.authOpenIDSub) {
|
|
|
|
extraData.authOpenIDSub = oldUser.authOpenIDSub
|
|
|
|
}
|
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
return {
|
|
|
|
id: oldUser.id,
|
|
|
|
username: oldUser.username,
|
2023-10-05 00:05:12 +02:00
|
|
|
email: oldUser.email || null,
|
2023-08-16 23:38:48 +02:00
|
|
|
pash: oldUser.pash || null,
|
|
|
|
type: oldUser.type || null,
|
|
|
|
token: oldUser.token || null,
|
|
|
|
isActive: !!oldUser.isActive,
|
|
|
|
lastSeen: oldUser.lastSeen || null,
|
2023-11-22 18:00:11 +01:00
|
|
|
extraData,
|
2023-08-16 23:38:48 +02:00
|
|
|
createdAt: oldUser.createdAt || Date.now(),
|
|
|
|
permissions: {
|
|
|
|
...oldUser.permissions,
|
|
|
|
librariesAccessible: oldUser.librariesAccessible || [],
|
|
|
|
itemTagsSelected: oldUser.itemTagsSelected || []
|
|
|
|
},
|
|
|
|
bookmarks: oldUser.bookmarks
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
static removeById(userId) {
|
|
|
|
return this.destroy({
|
|
|
|
where: {
|
|
|
|
id: userId
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-07-22 22:32:20 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Create root user
|
|
|
|
* @param {string} username
|
|
|
|
* @param {string} pash
|
|
|
|
* @param {Auth} auth
|
2023-11-22 18:00:11 +01:00
|
|
|
* @returns {Promise<oldUser>}
|
2023-08-16 23:38:48 +02:00
|
|
|
*/
|
|
|
|
static async createRootUser(username, pash, auth) {
|
|
|
|
const userId = uuidv4()
|
2023-07-22 22:32:20 +02:00
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
const token = await auth.generateAccessToken({ id: userId, username })
|
2023-07-22 22:32:20 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
const newRoot = new oldUser({
|
|
|
|
id: userId,
|
|
|
|
type: 'root',
|
|
|
|
username,
|
|
|
|
pash,
|
|
|
|
token,
|
|
|
|
isActive: true,
|
|
|
|
createdAt: Date.now()
|
|
|
|
})
|
|
|
|
await this.createFromOld(newRoot)
|
|
|
|
return newRoot
|
|
|
|
}
|
2023-07-22 22:32:20 +02:00
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
* Create user from openid userinfo
|
|
|
|
* @param {Object} userinfo
|
|
|
|
* @param {Auth} auth
|
|
|
|
* @returns {Promise<oldUser>}
|
|
|
|
*/
|
|
|
|
static async createUserFromOpenIdUserInfo(userinfo, auth) {
|
|
|
|
const userId = uuidv4()
|
|
|
|
// TODO: Ensure username is unique?
|
|
|
|
const username = userinfo.preferred_username || userinfo.name || userinfo.sub
|
|
|
|
const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null
|
|
|
|
|
|
|
|
const token = await auth.generateAccessToken({ id: userId, username })
|
|
|
|
|
|
|
|
const newUser = new oldUser({
|
|
|
|
id: userId,
|
|
|
|
type: 'user',
|
|
|
|
username,
|
|
|
|
email,
|
|
|
|
pash: null,
|
|
|
|
token,
|
|
|
|
isActive: true,
|
|
|
|
authOpenIDSub: userinfo.sub,
|
|
|
|
createdAt: Date.now()
|
|
|
|
})
|
|
|
|
if (await this.createFromOld(newUser)) {
|
|
|
|
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
|
|
|
return newUser
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get a user by id or by the old database id
|
|
|
|
* @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id
|
|
|
|
* @param {string} userId
|
|
|
|
* @returns {Promise<oldUser|null>} null if not found
|
|
|
|
*/
|
|
|
|
static async getUserByIdOrOldId(userId) {
|
|
|
|
if (!userId) return null
|
|
|
|
const user = await this.findOne({
|
|
|
|
where: {
|
2023-11-22 18:00:11 +01:00
|
|
|
[sequelize.Op.or]: [
|
2023-08-16 23:38:48 +02:00
|
|
|
{
|
|
|
|
id: userId
|
|
|
|
},
|
|
|
|
{
|
|
|
|
extraData: {
|
2023-11-22 18:00:11 +01:00
|
|
|
[sequelize.Op.substring]: userId
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
if (!user) return null
|
|
|
|
return this.getOldUser(user)
|
|
|
|
}
|
2023-07-22 22:32:20 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get user by username case insensitive
|
|
|
|
* @param {string} username
|
|
|
|
* @returns {Promise<oldUser|null>} returns null if not found
|
|
|
|
*/
|
|
|
|
static async getUserByUsername(username) {
|
|
|
|
if (!username) return null
|
|
|
|
const user = await this.findOne({
|
|
|
|
where: {
|
|
|
|
username: {
|
2023-11-22 18:00:11 +01:00
|
|
|
[sequelize.Op.like]: username
|
|
|
|
}
|
|
|
|
},
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
if (!user) return null
|
|
|
|
return this.getOldUser(user)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get user by email case insensitive
|
|
|
|
* @param {string} username
|
|
|
|
* @returns {Promise<oldUser|null>} returns null if not found
|
|
|
|
*/
|
|
|
|
static async getUserByEmail(email) {
|
|
|
|
if (!email) return null
|
|
|
|
const user = await this.findOne({
|
|
|
|
where: {
|
|
|
|
email: {
|
|
|
|
[sequelize.Op.like]: email
|
2023-07-22 22:32:20 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
},
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
if (!user) return null
|
|
|
|
return this.getOldUser(user)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get user by id
|
|
|
|
* @param {string} userId
|
|
|
|
* @returns {Promise<oldUser|null>} returns null if not found
|
|
|
|
*/
|
|
|
|
static async getUserById(userId) {
|
|
|
|
if (!userId) return null
|
|
|
|
const user = await this.findByPk(userId, {
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
if (!user) return null
|
|
|
|
return this.getOldUser(user)
|
|
|
|
}
|
|
|
|
|
2023-11-22 18:00:11 +01:00
|
|
|
/**
|
|
|
|
* Get user by openid sub
|
|
|
|
* @param {string} sub
|
|
|
|
* @returns {Promise<oldUser|null>} returns null if not found
|
|
|
|
*/
|
|
|
|
static async getUserByOpenIDSub(sub) {
|
|
|
|
if (!sub) return null
|
|
|
|
const user = await this.findOne({
|
|
|
|
where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
|
|
|
|
include: this.sequelize.models.mediaProgress
|
|
|
|
})
|
|
|
|
if (!user) return null
|
|
|
|
return this.getOldUser(user)
|
|
|
|
}
|
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get array of user id and username
|
|
|
|
* @returns {object[]} { id, username }
|
|
|
|
*/
|
|
|
|
static async getMinifiedUserObjects() {
|
|
|
|
const users = await this.findAll({
|
|
|
|
attributes: ['id', 'username']
|
|
|
|
})
|
|
|
|
return users.map(u => {
|
|
|
|
return {
|
|
|
|
id: u.id,
|
|
|
|
username: u.username
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return true if root user exists
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
static async getHasRootUser() {
|
|
|
|
const count = await this.count({
|
|
|
|
where: {
|
|
|
|
type: 'root'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return count > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize model
|
|
|
|
* @param {import('../Database').sequelize} sequelize
|
|
|
|
*/
|
|
|
|
static init(sequelize) {
|
|
|
|
super.init({
|
|
|
|
id: {
|
|
|
|
type: DataTypes.UUID,
|
|
|
|
defaultValue: DataTypes.UUIDV4,
|
|
|
|
primaryKey: true
|
|
|
|
},
|
|
|
|
username: DataTypes.STRING,
|
|
|
|
email: DataTypes.STRING,
|
|
|
|
pash: DataTypes.STRING,
|
|
|
|
type: DataTypes.STRING,
|
|
|
|
token: DataTypes.STRING,
|
|
|
|
isActive: {
|
|
|
|
type: DataTypes.BOOLEAN,
|
|
|
|
defaultValue: false
|
|
|
|
},
|
|
|
|
isLocked: {
|
|
|
|
type: DataTypes.BOOLEAN,
|
|
|
|
defaultValue: false
|
|
|
|
},
|
|
|
|
lastSeen: DataTypes.DATE,
|
|
|
|
permissions: DataTypes.JSON,
|
|
|
|
bookmarks: DataTypes.JSON,
|
|
|
|
extraData: DataTypes.JSON
|
|
|
|
}, {
|
|
|
|
sequelize,
|
|
|
|
modelName: 'user'
|
|
|
|
})
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
module.exports = User
|