X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Fuser.ts;h=56af2f30a979605248dfa3d8de08dce2cb328ae7;hb=0883b3245bf0deb9106c4041e9afbd3521b79280;hp=7390baf91d0ecfb001d7d0142b21cece9c735167;hpb=38fa2065831b5f55be0d7f30f19a62c967397208;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 7390baf91..56af2f30a 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -1,311 +1,302 @@ import * as Sequelize from 'sequelize' - -import { getSort, addMethodsToModel } from '../utils' import { - cryptPassword, - comparePassword, + AllowNull, + BeforeCreate, + BeforeUpdate, + Column, + CreatedAt, + DataType, + Default, + DefaultScope, + HasMany, + HasOne, + Is, + IsEmail, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' +import { User, UserRole } from '../../../shared/models/users' +import { + isUserAutoPlayVideoValid, + isUserNSFWPolicyValid, isUserPasswordValid, + isUserRoleValid, isUserUsernameValid, - isUserDisplayNSFWValid, - isUserVideoQuotaValid, - isUserRoleValid -} from '../../helpers' -import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared' - -import { - UserInstance, - UserAttributes, - - UserMethods -} from './user-interface' - -let User: Sequelize.Model -let isPasswordMatch: UserMethods.IsPasswordMatch -let hasRight: UserMethods.HasRight -let toFormattedJSON: UserMethods.ToFormattedJSON -let countTotal: UserMethods.CountTotal -let getByUsername: UserMethods.GetByUsername -let listForApi: UserMethods.ListForApi -let loadById: UserMethods.LoadById -let loadByUsername: UserMethods.LoadByUsername -let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels -let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail -let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - User = sequelize.define('User', + isUserVideoQuotaValid +} from '../../helpers/custom-validators/users' +import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' +import { OAuthTokenModel } from '../oauth/oauth-token' +import { getSort, throwIfNotValid } from '../utils' +import { VideoChannelModel } from '../video/video-channel' +import { AccountModel } from './account' +import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' +import { values } from 'lodash' +import { NSFW_POLICY_TYPES } from '../../initializers' + +@DefaultScope({ + include: [ { - password: { - type: DataTypes.STRING, - allowNull: false, - validate: { - passwordValid: value => { - const res = isUserPasswordValid(value) - if (res === false) throw new Error('Password not valid.') - } - } - }, - username: { - type: DataTypes.STRING, - allowNull: false, - validate: { - usernameValid: value => { - const res = isUserUsernameValid(value) - if (res === false) throw new Error('Username not valid.') - } - } - }, - email: { - type: DataTypes.STRING(400), - allowNull: false, - validate: { - isEmail: true - } - }, - displayNSFW: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - validate: { - nsfwValid: value => { - const res = isUserDisplayNSFWValid(value) - if (res === false) throw new Error('Display NSFW is not valid.') - } - } - }, - role: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - roleValid: value => { - const res = isUserRoleValid(value) - if (res === false) throw new Error('Role is not valid.') - } - } - }, - videoQuota: { - type: DataTypes.BIGINT, - allowNull: false, - validate: { - videoQuotaValid: value => { - const res = isUserVideoQuotaValid(value) - if (res === false) throw new Error('Video quota is not valid.') - } - } + model: () => AccountModel, + required: true + } + ] +}) +@Scopes({ + withVideoChannel: { + include: [ + { + model: () => AccountModel, + required: true, + include: [ () => VideoChannelModel ] } + ] + } +}) +@Table({ + tableName: 'user', + indexes: [ + { + fields: [ 'username' ], + unique: true }, { - indexes: [ - { - fields: [ 'username' ], - unique: true - }, - { - fields: [ 'email' ], - unique: true - } - ], - hooks: { - beforeCreate: beforeCreateOrUpdate, - beforeUpdate: beforeCreateOrUpdate - } + fields: [ 'email' ], + unique: true } - ) - - const classMethods = [ - associate, - - countTotal, - getByUsername, - listForApi, - loadById, - loadByUsername, - loadByUsernameAndPopulateChannels, - loadByUsernameOrEmail - ] - const instanceMethods = [ - hasRight, - isPasswordMatch, - toFormattedJSON, - isAbleToUploadVideo ] - addMethodsToModel(User, classMethods, instanceMethods) +}) +export class UserModel extends Model { + + @AllowNull(false) + @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password')) + @Column + password: string + + @AllowNull(false) + @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name')) + @Column + username: string + + @AllowNull(false) + @IsEmail + @Column(DataType.STRING(400)) + email: string + + @AllowNull(false) + @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) + @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) + nsfwPolicy: NSFWPolicyType + + @AllowNull(false) + @Default(true) + @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) + @Column + autoPlayVideo: boolean + + @AllowNull(false) + @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role')) + @Column + role: number + + @AllowNull(false) + @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota')) + @Column(DataType.BIGINT) + videoQuota: number + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @HasOne(() => AccountModel, { + foreignKey: 'userId', + onDelete: 'cascade', + hooks: true + }) + Account: AccountModel - return User -} + @HasMany(() => OAuthTokenModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + OAuthTokens: OAuthTokenModel[] + + @BeforeCreate + @BeforeUpdate + static cryptPasswordIfNeeded (instance: UserModel) { + if (instance.changed('password')) { + return cryptPassword(instance.password) + .then(hash => { + instance.password = hash + return undefined + }) + } + } -function beforeCreateOrUpdate (user: UserInstance) { - if (user.changed('password')) { - return cryptPassword(user.password) - .then(hash => { - user.password = hash - return undefined - }) + static countTotal () { + return this.count() } -} -// ------------------------------ METHODS ------------------------------ + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: getSort(sort) + } -hasRight = function (this: UserInstance, right: UserRight) { - return hasUserRight(this.role, right) -} + return UserModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } -isPasswordMatch = function (this: UserInstance, password: string) { - return comparePassword(password, this.password) -} + static listEmailsWithRight (right: UserRight) { + const roles = Object.keys(USER_ROLE_LABELS) + .map(k => parseInt(k, 10) as UserRole) + .filter(role => hasUserRight(role, right)) + + console.log(roles) -toFormattedJSON = function (this: UserInstance) { - const json = { - id: this.id, - username: this.username, - email: this.email, - displayNSFW: this.displayNSFW, - role: this.role, - roleLabel: USER_ROLE_LABELS[this.role], - videoQuota: this.videoQuota, - createdAt: this.createdAt, - account: { - id: this.Account.id, - uuid: this.Account.uuid + const query = { + attribute: [ 'email' ], + where: { + role: { + [Sequelize.Op.in]: roles + } + } } + + return UserModel.unscoped() + .findAll(query) + .then(u => u.map(u => u.email)) } - if (Array.isArray(this.Account.VideoChannels) === true) { - const videoChannels = this.Account.VideoChannels - .map(c => c.toFormattedJSON()) - .sort((v1, v2) => { - if (v1.createdAt < v2.createdAt) return -1 - if (v1.createdAt === v2.createdAt) return 0 + static loadById (id: number) { + return UserModel.findById(id) + } - return 1 - }) + static loadByUsername (username: string) { + const query = { + where: { + username + } + } - json['videoChannels'] = videoChannels + return UserModel.findOne(query) } - return json -} - -isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { - if (this.videoQuota === -1) return Promise.resolve(true) + static loadByUsernameAndPopulateChannels (username: string) { + const query = { + where: { + username + } + } - return getOriginalVideoFileTotalFromUser(this).then(totalBytes => { - return (videoFile.size + totalBytes) < this.videoQuota - }) -} + return UserModel.scope('withVideoChannel').findOne(query) + } -// ------------------------------ STATICS ------------------------------ + static loadByEmail (email: string) { + const query = { + where: { + email + } + } -function associate (models) { - User.hasOne(models.Account, { - foreignKey: 'userId', - onDelete: 'cascade' - }) + return UserModel.findOne(query) + } - User.hasMany(models.OAuthToken, { - foreignKey: 'userId', - onDelete: 'cascade' - }) -} + static loadByUsernameOrEmail (username: string, email?: string) { + if (!email) email = username -countTotal = function () { - return this.count() -} + const query = { + where: { + [ Sequelize.Op.or ]: [ { username }, { email } ] + } + } -getByUsername = function (username: string) { - const query = { - where: { - username: username - }, - include: [ { model: User['sequelize'].models.Account, required: true } ] + return UserModel.findOne(query) } - return User.findOne(query) -} + static getOriginalVideoFileTotalFromUser (user: UserModel) { + // Don't use sequelize because we need to use a sub query + const query = 'SELECT SUM("size") AS "total" FROM ' + + '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + + 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + + 'INNER JOIN "user" ON "account"."userId" = "user"."id" ' + + 'WHERE "user"."id" = $userId GROUP BY "video"."id") t' + + const options = { + bind: { userId: user.id }, + type: Sequelize.QueryTypes.SELECT + } + return UserModel.sequelize.query(query, options) + .then(([ { total } ]) => { + if (total === null) return 0 -listForApi = function (start: number, count: number, sort: string) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ { model: User['sequelize'].models.Account, required: true } ] + return parseInt(total, 10) + }) } - return User.findAndCountAll(query).then(({ rows, count }) => { + static async getStats () { + const totalUsers = await UserModel.count() + return { - data: rows, - total: count + totalUsers } - }) -} - -loadById = function (id: number) { - const options = { - include: [ { model: User['sequelize'].models.Account, required: true } ] } - return User.findById(id, options) -} - -loadByUsername = function (username: string) { - const query = { - where: { - username - }, - include: [ { model: User['sequelize'].models.Account, required: true } ] + hasRight (right: UserRight) { + return hasUserRight(this.role, right) } - return User.findOne(query) -} - -loadByUsernameAndPopulateChannels = function (username: string) { - const query = { - where: { - username - }, - include: [ - { - model: User['sequelize'].models.Account, - required: true, - include: [ User['sequelize'].models.VideoChannel ] - } - ] + isPasswordMatch (password: string) { + return comparePassword(password, this.password) } - return User.findOne(query) -} + toFormattedJSON (): User { + const json = { + id: this.id, + username: this.username, + email: this.email, + nsfwPolicy: this.nsfwPolicy, + autoPlayVideo: this.autoPlayVideo, + role: this.role, + roleLabel: USER_ROLE_LABELS[ this.role ], + videoQuota: this.videoQuota, + createdAt: this.createdAt, + account: this.Account.toFormattedJSON(), + videoChannels: [] + } + + if (Array.isArray(this.Account.VideoChannels) === true) { + json.videoChannels = this.Account.VideoChannels + .map(c => c.toFormattedJSON()) + .sort((v1, v2) => { + if (v1.createdAt < v2.createdAt) return -1 + if (v1.createdAt === v2.createdAt) return 0 -loadByUsernameOrEmail = function (username: string, email: string) { - const query = { - include: [ { model: User['sequelize'].models.Account, required: true } ], - where: { - [Sequelize.Op.or]: [ { username }, { email } ] + return 1 + }) } + + return json } - // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 - return (User as any).findOne(query) -} + isAbleToUploadVideo (videoFile: Express.Multer.File) { + if (this.videoQuota === -1) return Promise.resolve(true) -// --------------------------------------------------------------------------- - -function getOriginalVideoFileTotalFromUser (user: UserInstance) { - // Don't use sequelize because we need to use a sub query - const query = 'SELECT SUM("size") AS "total" FROM ' + - '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + - 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + - 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + - 'INNER JOIN "Accounts" ON "VideoChannels"."accountId" = "Accounts"."id" ' + - 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' + - 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' - - const options = { - bind: { userId: user.id }, - type: Sequelize.QueryTypes.SELECT + return UserModel.getOriginalVideoFileTotalFromUser(this) + .then(totalBytes => { + return (videoFile.size + totalBytes) < this.videoQuota + }) } - return User['sequelize'].query(query, options).then(([ { total } ]) => { - if (total === null) return 0 - - return parseInt(total, 10) - }) }