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