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