X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fuser%2Fuser.ts;h=bfc9b30495a55791034fce1768802307949f02fc;hb=4e4c23c5b8d55ab0aa48a7be8c53ec71d1d7e796;hp=20696b1f49fb04d0ed8e9f1ae1186c91d6ff6272;hpb=8ee37c5f38b0f9b7e97239197d5590109c163250;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 20696b1f4..bfc9b3049 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -1,4 +1,3 @@ -import { values } from 'lodash' import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize' import { AfterDestroy, @@ -22,25 +21,24 @@ import { UpdatedAt } from 'sequelize-typescript' import { TokensCache } from '@server/lib/auth/tokens-cache' +import { LiveQuotaStore } from '@server/lib/live' import { MMyUserFormattable, MUser, MUserDefault, MUserFormattable, MUserNotifSettingChannelDefault, - MUserWithNotificationSetting, - MVideoWithRights + MUserWithNotificationSetting } from '@server/types/models' -import { AttributesOnly } from '@shared/core-utils' +import { forceNumber } from '@shared/core-utils' +import { AttributesOnly } from '@shared/typescript-utils' import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' -import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' +import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' import { User, UserRole } from '../../../shared/models/users' import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { - isNoInstanceConfigWarningModal, - isNoWelcomeModal, isUserAdminFlagsValid, isUserAutoPlayNextVideoPlaylistValid, isUserAutoPlayNextVideoValid, @@ -48,15 +46,15 @@ import { isUserBlockedReasonValid, isUserBlockedValid, isUserEmailVerifiedValid, + isUserNoModal, isUserNSFWPolicyValid, + isUserP2PEnabledValid, isUserPasswordValid, isUserRoleValid, - isUserUsernameValid, isUserVideoLanguages, isUserVideoQuotaDailyValid, isUserVideoQuotaValid, - isUserVideosHistoryEnabledValid, - isUserWebTorrentEnabledValid + isUserVideosHistoryEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' @@ -66,7 +64,7 @@ import { ActorModel } from '../actor/actor' import { ActorFollowModel } from '../actor/actor-follow' import { ActorImageModel } from '../actor/actor-image' import { OAuthTokenModel } from '../oauth/oauth-token' -import { getSort, throwIfNotValid } from '../utils' +import { getAdminUsersSort, throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { VideoImportModel } from '../video/video-import' @@ -77,6 +75,7 @@ import { UserNotificationSettingModel } from './user-notification-setting' enum ScopeNames { FOR_ME_API = 'FOR_ME_API', WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', + WITH_QUOTA = 'WITH_QUOTA', WITH_STATS = 'WITH_STATS' } @@ -107,7 +106,7 @@ enum ScopeNames { include: [ { model: ActorImageModel, - as: 'Banner', + as: 'Banners', required: false } ] @@ -154,7 +153,7 @@ enum ScopeNames { } ] }, - [ScopeNames.WITH_STATS]: { + [ScopeNames.WITH_QUOTA]: { attributes: { include: [ [ @@ -162,12 +161,31 @@ enum ScopeNames { '(' + UserModel.generateUserQuotaBaseSQL({ withSelect: false, - whereUserId: '"UserModel"."id"' + whereUserId: '"UserModel"."id"', + daily: false }) + ')' ), 'videoQuotaUsed' ], + [ + literal( + '(' + + UserModel.generateUserQuotaBaseSQL({ + withSelect: false, + whereUserId: '"UserModel"."id"', + daily: true + }) + + ')' + ), + 'videoQuotaUsedDaily' + ] + ] + } + }, + [ScopeNames.WITH_STATS]: { + attributes: { + include: [ [ literal( '(' + @@ -242,7 +260,6 @@ export class UserModel extends Model>> { password: string @AllowNull(false) - @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name')) @Column username: string @@ -264,14 +281,13 @@ export class UserModel extends Model>> { @AllowNull(false) @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) - @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES))) + @Column(DataType.ENUM(...Object.values(NSFW_POLICY_TYPES))) nsfwPolicy: NSFWPolicyType @AllowNull(false) - @Default(true) - @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) + @Is('p2pEnabled', value => throwIfNotValid(value, isUserP2PEnabledValid, 'P2P enabled')) @Column - webTorrentEnabled: boolean + p2pEnabled: boolean @AllowNull(false) @Default(true) @@ -349,7 +365,7 @@ export class UserModel extends Model>> { @Default(false) @Is( 'UserNoInstanceConfigWarningModal', - value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal') + value => throwIfNotValid(value, isUserNoModal, 'no instance config warning modal') ) @Column noInstanceConfigWarningModal: boolean @@ -357,12 +373,21 @@ export class UserModel extends Model>> { @AllowNull(false) @Default(false) @Is( - 'UserNoInstanceConfigWarningModal', - value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal') + 'UserNoWelcomeModal', + value => throwIfNotValid(value, isUserNoModal, 'no welcome modal') ) @Column noWelcomeModal: boolean + @AllowNull(false) + @Default(false) + @Is( + 'UserNoAccountSetupWarningModal', + value => throwIfNotValid(value, isUserNoModal, 'no account setup warning modal') + ) + @Column + noAccountSetupWarningModal: boolean + @AllowNull(true) @Default(null) @Column @@ -379,6 +404,11 @@ export class UserModel extends Model>> { @Column lastLoginDate: Date + @AllowNull(true) + @Default(null) + @Column + otpSecret: string + @CreatedAt createdAt: Date @@ -411,16 +441,17 @@ export class UserModel extends Model>> { }) OAuthTokens: OAuthTokenModel[] + // Used if we already set an encrypted password in user model + skipPasswordEncryption = false + @BeforeCreate @BeforeUpdate - static cryptPasswordIfNeeded (instance: UserModel) { - if (instance.changed('password') && instance.password) { - return cryptPassword(instance.password) - .then(hash => { - instance.password = hash - return undefined - }) - } + static async cryptPasswordIfNeeded (instance: UserModel) { + if (instance.skipPasswordEncryption) return + if (!instance.changed('password')) return + if (!instance.password) return + + instance.password = await cryptPassword(instance.password) } @AfterUpdate @@ -430,10 +461,10 @@ export class UserModel extends Model>> { } static countTotal () { - return this.count() + return UserModel.unscoped().count() } - static listForApi (parameters: { + static listForAdminApi (parameters: { start: number count: number sort: string @@ -461,40 +492,20 @@ export class UserModel extends Model>> { } if (blocked !== undefined) { - Object.assign(where, { - blocked: blocked - }) + Object.assign(where, { blocked }) } const query: FindOptions = { - attributes: { - include: [ - [ - literal( - '(' + - UserModel.generateUserQuotaBaseSQL({ - withSelect: false, - whereUserId: '"UserModel"."id"' - }) + - ')' - ), - 'videoQuotaUsed' - ] as any // FIXME: typings - ] - }, offset: start, limit: count, - order: getSort(sort), + order: getAdminUsersSort(sort), where } - return UserModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) + return Promise.all([ + UserModel.unscoped().count(query), + UserModel.scope([ 'defaultScope', ScopeNames.WITH_QUOTA ]).findAll(query) + ]).then(([ total, data ]) => ({ total, data })) } static listWithRight (right: UserRight): Promise { @@ -575,7 +586,10 @@ export class UserModel extends Model>> { ScopeNames.WITH_VIDEOCHANNELS ] - if (withStats) scopes.push(ScopeNames.WITH_STATS) + if (withStats) { + scopes.push(ScopeNames.WITH_QUOTA) + scopes.push(ScopeNames.WITH_STATS) + } return UserModel.scope(scopes).findByPk(id) } @@ -616,7 +630,7 @@ export class UserModel extends Model>> { const query = { where: { [Op.or]: [ - where(fn('lower', col('username')), fn('lower', username)), + where(fn('lower', col('username')), fn('lower', username) as any), { email } ] @@ -756,10 +770,10 @@ export class UserModel extends Model>> { static generateUserQuotaBaseSQL (options: { whereUserId: '$userId' | '"UserModel"."id"' withSelect: boolean - where?: string + daily: boolean }) { - const andWhere = options.where - ? 'AND ' + options.where + const andWhere = options.daily === true + ? 'AND "video"."createdAt" > now() - interval \'24 hours\'' : '' const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + @@ -806,10 +820,10 @@ export class UserModel extends Model>> { } } - return UserModel.count(query) + return UserModel.unscoped().count(query) } - const totalUsers = await UserModel.count() + const totalUsers = await UserModel.unscoped().count() const totalDailyActiveUsers = await getActiveUsers(1) const totalWeeklyActiveUsers = await getActiveUsers(7) const totalMonthlyActiveUsers = await getActiveUsers(30) @@ -838,22 +852,6 @@ export class UserModel extends Model>> { .then(u => u.map(u => u.username)) } - canGetVideo (video: MVideoWithRights) { - const videoUserId = video.VideoChannel.Account.userId - - if (video.isBlacklisted()) { - return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) - } - - if (video.privacy === VideoPrivacy.PRIVATE) { - return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) - } - - if (video.privacy === VideoPrivacy.INTERNAL) return true - - return false - } - hasRight (right: UserRight) { return hasUserRight(this.role, right) } @@ -884,42 +882,52 @@ export class UserModel extends Model>> { emailVerified: this.emailVerified, nsfwPolicy: this.nsfwPolicy, - webTorrentEnabled: this.webTorrentEnabled, + + // FIXME: deprecated in 4.1 + webTorrentEnabled: this.p2pEnabled, + p2pEnabled: this.p2pEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, autoPlayVideo: this.autoPlayVideo, autoPlayNextVideo: this.autoPlayNextVideo, autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, videoLanguages: this.videoLanguages, - role: this.role, - roleLabel: USER_ROLE_LABELS[this.role], + role: { + id: this.role, + label: USER_ROLE_LABELS[this.role] + }, videoQuota: this.videoQuota, videoQuotaDaily: this.videoQuotaDaily, + videoQuotaUsed: videoQuotaUsed !== undefined - ? parseInt(videoQuotaUsed + '', 10) + ? forceNumber(videoQuotaUsed) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) : undefined, + videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined - ? parseInt(videoQuotaUsedDaily + '', 10) + ? forceNumber(videoQuotaUsedDaily) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) : undefined, + videosCount: videosCount !== undefined - ? parseInt(videosCount + '', 10) + ? forceNumber(videosCount) : undefined, abusesCount: abusesCount - ? parseInt(abusesCount, 10) + ? forceNumber(abusesCount) : undefined, abusesAcceptedCount: abusesAcceptedCount - ? parseInt(abusesAcceptedCount, 10) + ? forceNumber(abusesAcceptedCount) : undefined, abusesCreatedCount: abusesCreatedCount !== undefined - ? parseInt(abusesCreatedCount + '', 10) + ? forceNumber(abusesCreatedCount) : undefined, videoCommentsCount: videoCommentsCount !== undefined - ? parseInt(videoCommentsCount + '', 10) + ? forceNumber(videoCommentsCount) : undefined, noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, noWelcomeModal: this.noWelcomeModal, + noAccountSetupWarningModal: this.noAccountSetupWarningModal, blocked: this.blocked, blockedReason: this.blockedReason, @@ -936,7 +944,9 @@ export class UserModel extends Model>> { pluginAuth: this.pluginAuth, - lastLoginDate: this.lastLoginDate + lastLoginDate: this.lastLoginDate, + + twoFactorEnabled: !!this.otpSecret } if (parameters.withAdminFlags) { @@ -958,7 +968,7 @@ export class UserModel extends Model>> { } toMeFormattedJSON (this: MMyUserFormattable): MyUser { - const formatted = this.toFormattedJSON() + const formatted = this.toFormattedJSON({ withAdminFlags: true }) const specialPlaylists = this.Account.VideoPlaylists .map(p => ({ id: p.id, name: p.name, type: p.type }))