]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user.ts
Implement user blocking on server side
[github/Chocobozzz/PeerTube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2 import {
3 AllowNull,
4 BeforeCreate,
5 BeforeUpdate,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 DefaultScope,
11 HasMany,
12 HasOne,
13 Is,
14 IsEmail,
15 Model,
16 Scopes,
17 Table,
18 UpdatedAt
19 } from 'sequelize-typescript'
20 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
21 import { User, UserRole } from '../../../shared/models/users'
22 import {
23 isUserAutoPlayVideoValid,
24 isUserBlockedValid,
25 isUserNSFWPolicyValid,
26 isUserPasswordValid,
27 isUserRoleValid,
28 isUserUsernameValid,
29 isUserVideoQuotaValid
30 } from '../../helpers/custom-validators/users'
31 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
32 import { OAuthTokenModel } from '../oauth/oauth-token'
33 import { getSort, throwIfNotValid } from '../utils'
34 import { VideoChannelModel } from '../video/video-channel'
35 import { AccountModel } from './account'
36 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
37 import { values } from 'lodash'
38 import { NSFW_POLICY_TYPES } from '../../initializers'
39
40 enum ScopeNames {
41 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
42 }
43
44 @DefaultScope({
45 include: [
46 {
47 model: () => AccountModel,
48 required: true
49 }
50 ]
51 })
52 @Scopes({
53 [ScopeNames.WITH_VIDEO_CHANNEL]: {
54 include: [
55 {
56 model: () => AccountModel,
57 required: true,
58 include: [ () => VideoChannelModel ]
59 }
60 ]
61 }
62 })
63 @Table({
64 tableName: 'user',
65 indexes: [
66 {
67 fields: [ 'username' ],
68 unique: true
69 },
70 {
71 fields: [ 'email' ],
72 unique: true
73 }
74 ]
75 })
76 export class UserModel extends Model<UserModel> {
77
78 @AllowNull(false)
79 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
80 @Column
81 password: string
82
83 @AllowNull(false)
84 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
85 @Column
86 username: string
87
88 @AllowNull(false)
89 @IsEmail
90 @Column(DataType.STRING(400))
91 email: string
92
93 @AllowNull(false)
94 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
95 @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
96 nsfwPolicy: NSFWPolicyType
97
98 @AllowNull(false)
99 @Default(true)
100 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
101 @Column
102 autoPlayVideo: boolean
103
104 @AllowNull(false)
105 @Default(false)
106 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
107 @Column
108 blocked: boolean
109
110 @AllowNull(false)
111 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
112 @Column
113 role: number
114
115 @AllowNull(false)
116 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
117 @Column(DataType.BIGINT)
118 videoQuota: number
119
120 @CreatedAt
121 createdAt: Date
122
123 @UpdatedAt
124 updatedAt: Date
125
126 @HasOne(() => AccountModel, {
127 foreignKey: 'userId',
128 onDelete: 'cascade',
129 hooks: true
130 })
131 Account: AccountModel
132
133 @HasMany(() => OAuthTokenModel, {
134 foreignKey: 'userId',
135 onDelete: 'cascade'
136 })
137 OAuthTokens: OAuthTokenModel[]
138
139 @BeforeCreate
140 @BeforeUpdate
141 static cryptPasswordIfNeeded (instance: UserModel) {
142 if (instance.changed('password')) {
143 return cryptPassword(instance.password)
144 .then(hash => {
145 instance.password = hash
146 return undefined
147 })
148 }
149 }
150
151 static countTotal () {
152 return this.count()
153 }
154
155 static listForApi (start: number, count: number, sort: string) {
156 const query = {
157 offset: start,
158 limit: count,
159 order: getSort(sort)
160 }
161
162 return UserModel.findAndCountAll(query)
163 .then(({ rows, count }) => {
164 return {
165 data: rows,
166 total: count
167 }
168 })
169 }
170
171 static listEmailsWithRight (right: UserRight) {
172 const roles = Object.keys(USER_ROLE_LABELS)
173 .map(k => parseInt(k, 10) as UserRole)
174 .filter(role => hasUserRight(role, right))
175
176 console.log(roles)
177
178 const query = {
179 attribute: [ 'email' ],
180 where: {
181 role: {
182 [Sequelize.Op.in]: roles
183 }
184 }
185 }
186
187 return UserModel.unscoped()
188 .findAll(query)
189 .then(u => u.map(u => u.email))
190 }
191
192 static loadById (id: number) {
193 return UserModel.findById(id)
194 }
195
196 static loadByUsername (username: string) {
197 const query = {
198 where: {
199 username
200 }
201 }
202
203 return UserModel.findOne(query)
204 }
205
206 static loadByUsernameAndPopulateChannels (username: string) {
207 const query = {
208 where: {
209 username
210 }
211 }
212
213 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
214 }
215
216 static loadByEmail (email: string) {
217 const query = {
218 where: {
219 email
220 }
221 }
222
223 return UserModel.findOne(query)
224 }
225
226 static loadByUsernameOrEmail (username: string, email?: string) {
227 if (!email) email = username
228
229 const query = {
230 where: {
231 [ Sequelize.Op.or ]: [ { username }, { email } ]
232 }
233 }
234
235 return UserModel.findOne(query)
236 }
237
238 static getOriginalVideoFileTotalFromUser (user: UserModel) {
239 // Don't use sequelize because we need to use a sub query
240 const query = 'SELECT SUM("size") AS "total" FROM ' +
241 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
242 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
243 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
244 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
245 'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
246 'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
247
248 const options = {
249 bind: { userId: user.id },
250 type: Sequelize.QueryTypes.SELECT
251 }
252 return UserModel.sequelize.query(query, options)
253 .then(([ { total } ]) => {
254 if (total === null) return 0
255
256 return parseInt(total, 10)
257 })
258 }
259
260 static async getStats () {
261 const totalUsers = await UserModel.count()
262
263 return {
264 totalUsers
265 }
266 }
267
268 hasRight (right: UserRight) {
269 return hasUserRight(this.role, right)
270 }
271
272 isPasswordMatch (password: string) {
273 return comparePassword(password, this.password)
274 }
275
276 toFormattedJSON (): User {
277 const json = {
278 id: this.id,
279 username: this.username,
280 email: this.email,
281 nsfwPolicy: this.nsfwPolicy,
282 autoPlayVideo: this.autoPlayVideo,
283 role: this.role,
284 roleLabel: USER_ROLE_LABELS[ this.role ],
285 videoQuota: this.videoQuota,
286 createdAt: this.createdAt,
287 account: this.Account.toFormattedJSON(),
288 videoChannels: []
289 }
290
291 if (Array.isArray(this.Account.VideoChannels) === true) {
292 json.videoChannels = this.Account.VideoChannels
293 .map(c => c.toFormattedJSON())
294 .sort((v1, v2) => {
295 if (v1.createdAt < v2.createdAt) return -1
296 if (v1.createdAt === v2.createdAt) return 0
297
298 return 1
299 })
300 }
301
302 return json
303 }
304
305 isAbleToUploadVideo (videoFile: { size: number }) {
306 if (this.videoQuota === -1) return Promise.resolve(true)
307
308 return UserModel.getOriginalVideoFileTotalFromUser(this)
309 .then(totalBytes => {
310 return (videoFile.size + totalBytes) < this.videoQuota
311 })
312 }
313 }