X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Fuser.ts;h=ea017c338fbdd4f6d73b0bac60677028d0f62490;hb=8b9a525a180cc9f3a98c334cc052dcfc8f36dcd4;hp=e37fd4d3be71ddc29195547187bcf0f83b9071c9;hpb=ce5496d6a31b9617aba67970f5dc135e73335234;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/user.ts b/server/models/account/user.ts index e37fd4d3b..ea017c338 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -1,19 +1,53 @@ import * as Sequelize from 'sequelize' import { - AllowNull, BeforeCreate, BeforeUpdate, Column, CreatedAt, DataType, Default, DefaultScope, HasMany, HasOne, Is, IsEmail, Model, - Scopes, Table, UpdatedAt + AfterDestroy, + AfterUpdate, + 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 } from '../../../shared/models/users' +import { User, UserRole } from '../../../shared/models/users' import { - isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, - isUserVideoQuotaValid + isUserAutoPlayVideoValid, + isUserBlockedReasonValid, + isUserBlockedValid, + isUserEmailVerifiedValid, + isUserNSFWPolicyValid, + isUserPasswordValid, + isUserRoleValid, + isUserUsernameValid, + isUserVideoQuotaDailyValid, + isUserVideoQuotaValid, + isUserWebTorrentEnabledValid, + isUserVideosHistoryEnabledValid } 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' +import { clearCacheByUserId } from '../../lib/oauth-model' + +enum ScopeNames { + WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' +} @DefaultScope({ include: [ @@ -24,7 +58,7 @@ import { AccountModel } from './account' ] }) @Scopes({ - withVideoChannel: { + [ScopeNames.WITH_VIDEO_CHANNEL]: { include: [ { model: () => AccountModel, @@ -64,11 +98,28 @@ export class UserModel extends Model { @Column(DataType.STRING(400)) email: string + @AllowNull(true) + @Default(null) + @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean')) + @Column + emailVerified: boolean + @AllowNull(false) - @Default(false) - @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) + @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) + @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) + nsfwPolicy: NSFWPolicyType + + @AllowNull(false) + @Default(true) + @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) @Column - displayNSFW: boolean + webTorrentEnabled: boolean + + @AllowNull(false) + @Default(true) + @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) + @Column + videosHistoryEnabled: boolean @AllowNull(false) @Default(true) @@ -76,6 +127,18 @@ export class UserModel extends Model { @Column autoPlayVideo: boolean + @AllowNull(false) + @Default(false) + @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean')) + @Column + blocked: boolean + + @AllowNull(true) + @Default(null) + @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason')) + @Column + blockedReason: string + @AllowNull(false) @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role')) @Column @@ -86,6 +149,11 @@ export class UserModel extends Model { @Column(DataType.BIGINT) videoQuota: number + @AllowNull(false) + @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily')) + @Column(DataType.BIGINT) + videoQuotaDaily: number + @CreatedAt createdAt: Date @@ -94,7 +162,8 @@ export class UserModel extends Model { @HasOne(() => AccountModel, { foreignKey: 'userId', - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) Account: AccountModel @@ -116,26 +185,59 @@ export class UserModel extends Model { } } + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserModel) { + return clearCacheByUserId(instance.id) + } + static countTotal () { return this.count() } - static getByUsername (username: string) { - const query = { - where: { - username: username - }, - include: [ { model: AccountModel, required: true } ] + static listForApi (start: number, count: number, sort: string, search?: string) { + let where = undefined + if (search) { + where = { + [Sequelize.Op.or]: [ + { + email: { + [Sequelize.Op.iLike]: '%' + search + '%' + } + }, + { + username: { + [ Sequelize.Op.iLike ]: '%' + search + '%' + } + } + ] + } } - return UserModel.findOne(query) - } - - static listForApi (start: number, count: number, sort: string) { const query = { + attributes: { + include: [ + [ + Sequelize.literal( + '(' + + 'SELECT COALESCE(SUM("size"), 0) ' + + '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" ' + + 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' + + ') t' + + ')' + ), + 'videoQuotaUsed' + ] as any // FIXME: typings + ] + }, offset: start, limit: count, - order: [ getSort(sort) ] + order: getSort(sort), + where } return UserModel.findAndCountAll(query) @@ -147,6 +249,25 @@ export class UserModel extends Model { }) } + static listEmailsWithRight (right: UserRight) { + const roles = Object.keys(USER_ROLE_LABELS) + .map(k => parseInt(k, 10) as UserRole) + .filter(role => hasUserRight(role, right)) + + const query = { + attribute: [ 'email' ], + where: { + role: { + [Sequelize.Op.in]: roles + } + } + } + + return UserModel.unscoped() + .findAll(query) + .then(u => u.map(u => u.email)) + } + static loadById (id: number) { return UserModel.findById(id) } @@ -168,10 +289,22 @@ export class UserModel extends Model { } } - return UserModel.scope('withVideoChannel').findOne(query) + return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query) + } + + static loadByEmail (email: string) { + const query = { + where: { + email + } + } + + return UserModel.findOne(query) } - static loadByUsernameOrEmail (username: string, email: string) { + static loadByUsernameOrEmail (username: string, email?: string) { + if (!email) email = username + const query = { where: { [ Sequelize.Op.or ]: [ { username }, { email } ] @@ -183,24 +316,39 @@ export class UserModel extends Model { 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 query = UserModel.generateUserQuotaBaseSQL() - const options = { - bind: { userId: user.id }, - type: Sequelize.QueryTypes.SELECT + return UserModel.getTotalRawQuery(query, user.id) + } + + // Returns cumulative size of all video files uploaded in the last 24 hours. + static getOriginalVideoFileTotalDailyFromUser (user: UserModel) { + // Don't use sequelize because we need to use a sub query + const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'') + + return UserModel.getTotalRawQuery(query, user.id) + } + + static async getStats () { + const totalUsers = await UserModel.count() + + return { + totalUsers } - return UserModel.sequelize.query(query, options) - .then(([ { total } ]) => { - if (total === null) return 0 + } - return parseInt(total, 10) - }) + static autoComplete (search: string) { + const query = { + where: { + username: { + [ Sequelize.Op.like ]: `%${search}%` + } + }, + limit: 10 + } + + return UserModel.findAll(query) + .then(u => u.map(u => u.username)) } hasRight (right: UserRight) { @@ -212,18 +360,32 @@ export class UserModel extends Model { } toFormattedJSON (): User { + const videoQuotaUsed = this.get('videoQuotaUsed') + const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') + const json = { id: this.id, username: this.username, email: this.email, - displayNSFW: this.displayNSFW, + emailVerified: this.emailVerified, + nsfwPolicy: this.nsfwPolicy, + webTorrentEnabled: this.webTorrentEnabled, autoPlayVideo: this.autoPlayVideo, role: this.role, roleLabel: USER_ROLE_LABELS[ this.role ], videoQuota: this.videoQuota, + videoQuotaDaily: this.videoQuotaDaily, createdAt: this.createdAt, + blocked: this.blocked, + blockedReason: this.blockedReason, account: this.Account.toFormattedJSON(), - videoChannels: [] + videoChannels: [], + videoQuotaUsed: videoQuotaUsed !== undefined + ? parseInt(videoQuotaUsed, 10) + : undefined, + videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined + ? parseInt(videoQuotaUsedDaily, 10) + : undefined } if (Array.isArray(this.Account.VideoChannels) === true) { @@ -240,12 +402,52 @@ export class UserModel extends Model { return json } - isAbleToUploadVideo (videoFile: Express.Multer.File) { - if (this.videoQuota === -1) return Promise.resolve(true) + async isAbleToUploadVideo (videoFile: { size: number }) { + if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true) - return UserModel.getOriginalVideoFileTotalFromUser(this) - .then(totalBytes => { - return (videoFile.size + totalBytes) < this.videoQuota - }) + const [ totalBytes, totalBytesDaily ] = await Promise.all([ + UserModel.getOriginalVideoFileTotalFromUser(this), + UserModel.getOriginalVideoFileTotalDailyFromUser(this) + ]) + + const uploadedTotal = videoFile.size + totalBytes + const uploadedDaily = videoFile.size + totalBytesDaily + if (this.videoQuotaDaily === -1) { + return uploadedTotal < this.videoQuota + } + if (this.videoQuota === -1) { + return uploadedDaily < this.videoQuotaDaily + } + + return (uploadedTotal < this.videoQuota) && + (uploadedDaily < this.videoQuotaDaily) + } + + private static generateUserQuotaBaseSQL (where?: string) { + const andWhere = where ? 'AND ' + where : '' + + return '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" ' + + 'WHERE "account"."userId" = $userId ' + andWhere + + 'GROUP BY "video"."id"' + + ') t' + } + + private static getTotalRawQuery (query: string, userId: number) { + const options = { + bind: { userId }, + type: Sequelize.QueryTypes.SELECT + } + + return UserModel.sequelize.query(query, options) + .then(([ { total } ]) => { + if (total === null) return 0 + + return parseInt(total, 10) + }) } }