user.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import * as Sequelize from 'sequelize'
  2. import {
  3. AfterDestroy,
  4. AfterUpdate,
  5. AllowNull,
  6. BeforeCreate,
  7. BeforeUpdate,
  8. Column,
  9. CreatedAt,
  10. DataType,
  11. Default,
  12. DefaultScope,
  13. HasMany,
  14. HasOne,
  15. Is,
  16. IsEmail,
  17. Model,
  18. Scopes,
  19. Table,
  20. UpdatedAt
  21. } from 'sequelize-typescript'
  22. import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
  23. import { User, UserRole } from '../../../shared/models/users'
  24. import {
  25. isUserAutoPlayVideoValid,
  26. isUserBlockedReasonValid,
  27. isUserBlockedValid,
  28. isUserEmailVerifiedValid,
  29. isUserNSFWPolicyValid,
  30. isUserPasswordValid,
  31. isUserRoleValid,
  32. isUserUsernameValid,
  33. isUserVideoQuotaDailyValid,
  34. isUserVideoQuotaValid,
  35. isUserWebTorrentEnabledValid,
  36. isUserVideosHistoryEnabledValid
  37. } from '../../helpers/custom-validators/users'
  38. import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
  39. import { OAuthTokenModel } from '../oauth/oauth-token'
  40. import { getSort, throwIfNotValid } from '../utils'
  41. import { VideoChannelModel } from '../video/video-channel'
  42. import { AccountModel } from './account'
  43. import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
  44. import { values } from 'lodash'
  45. import { NSFW_POLICY_TYPES } from '../../initializers'
  46. import { clearCacheByUserId } from '../../lib/oauth-model'
  47. enum ScopeNames {
  48. WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
  49. }
  50. @DefaultScope({
  51. include: [
  52. {
  53. model: () => AccountModel,
  54. required: true
  55. }
  56. ]
  57. })
  58. @Scopes({
  59. [ScopeNames.WITH_VIDEO_CHANNEL]: {
  60. include: [
  61. {
  62. model: () => AccountModel,
  63. required: true,
  64. include: [ () => VideoChannelModel ]
  65. }
  66. ]
  67. }
  68. })
  69. @Table({
  70. tableName: 'user',
  71. indexes: [
  72. {
  73. fields: [ 'username' ],
  74. unique: true
  75. },
  76. {
  77. fields: [ 'email' ],
  78. unique: true
  79. }
  80. ]
  81. })
  82. export class UserModel extends Model<UserModel> {
  83. @AllowNull(false)
  84. @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
  85. @Column
  86. password: string
  87. @AllowNull(false)
  88. @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
  89. @Column
  90. username: string
  91. @AllowNull(false)
  92. @IsEmail
  93. @Column(DataType.STRING(400))
  94. email: string
  95. @AllowNull(true)
  96. @Default(null)
  97. @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean'))
  98. @Column
  99. emailVerified: boolean
  100. @AllowNull(false)
  101. @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
  102. @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
  103. nsfwPolicy: NSFWPolicyType
  104. @AllowNull(false)
  105. @Default(true)
  106. @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
  107. @Column
  108. webTorrentEnabled: boolean
  109. @AllowNull(false)
  110. @Default(true)
  111. @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
  112. @Column
  113. videosHistoryEnabled: boolean
  114. @AllowNull(false)
  115. @Default(true)
  116. @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
  117. @Column
  118. autoPlayVideo: boolean
  119. @AllowNull(false)
  120. @Default(false)
  121. @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
  122. @Column
  123. blocked: boolean
  124. @AllowNull(true)
  125. @Default(null)
  126. @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
  127. @Column
  128. blockedReason: string
  129. @AllowNull(false)
  130. @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
  131. @Column
  132. role: number
  133. @AllowNull(false)
  134. @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
  135. @Column(DataType.BIGINT)
  136. videoQuota: number
  137. @AllowNull(false)
  138. @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
  139. @Column(DataType.BIGINT)
  140. videoQuotaDaily: number
  141. @CreatedAt
  142. createdAt: Date
  143. @UpdatedAt
  144. updatedAt: Date
  145. @HasOne(() => AccountModel, {
  146. foreignKey: 'userId',
  147. onDelete: 'cascade',
  148. hooks: true
  149. })
  150. Account: AccountModel
  151. @HasMany(() => OAuthTokenModel, {
  152. foreignKey: 'userId',
  153. onDelete: 'cascade'
  154. })
  155. OAuthTokens: OAuthTokenModel[]
  156. @BeforeCreate
  157. @BeforeUpdate
  158. static cryptPasswordIfNeeded (instance: UserModel) {
  159. if (instance.changed('password')) {
  160. return cryptPassword(instance.password)
  161. .then(hash => {
  162. instance.password = hash
  163. return undefined
  164. })
  165. }
  166. }
  167. @AfterUpdate
  168. @AfterDestroy
  169. static removeTokenCache (instance: UserModel) {
  170. return clearCacheByUserId(instance.id)
  171. }
  172. static countTotal () {
  173. return this.count()
  174. }
  175. static listForApi (start: number, count: number, sort: string, search?: string) {
  176. let where = undefined
  177. if (search) {
  178. where = {
  179. [Sequelize.Op.or]: [
  180. {
  181. email: {
  182. [Sequelize.Op.iLike]: '%' + search + '%'
  183. }
  184. },
  185. {
  186. username: {
  187. [ Sequelize.Op.iLike ]: '%' + search + '%'
  188. }
  189. }
  190. ]
  191. }
  192. }
  193. const query = {
  194. attributes: {
  195. include: [
  196. [
  197. Sequelize.literal(
  198. '(' +
  199. 'SELECT COALESCE(SUM("size"), 0) ' +
  200. 'FROM (' +
  201. 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
  202. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
  203. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  204. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  205. 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
  206. ') t' +
  207. ')'
  208. ),
  209. 'videoQuotaUsed'
  210. ] as any // FIXME: typings
  211. ]
  212. },
  213. offset: start,
  214. limit: count,
  215. order: getSort(sort),
  216. where
  217. }
  218. return UserModel.findAndCountAll(query)
  219. .then(({ rows, count }) => {
  220. return {
  221. data: rows,
  222. total: count
  223. }
  224. })
  225. }
  226. static listEmailsWithRight (right: UserRight) {
  227. const roles = Object.keys(USER_ROLE_LABELS)
  228. .map(k => parseInt(k, 10) as UserRole)
  229. .filter(role => hasUserRight(role, right))
  230. const query = {
  231. attribute: [ 'email' ],
  232. where: {
  233. role: {
  234. [Sequelize.Op.in]: roles
  235. }
  236. }
  237. }
  238. return UserModel.unscoped()
  239. .findAll(query)
  240. .then(u => u.map(u => u.email))
  241. }
  242. static loadById (id: number) {
  243. return UserModel.findById(id)
  244. }
  245. static loadByUsername (username: string) {
  246. const query = {
  247. where: {
  248. username
  249. }
  250. }
  251. return UserModel.findOne(query)
  252. }
  253. static loadByUsernameAndPopulateChannels (username: string) {
  254. const query = {
  255. where: {
  256. username
  257. }
  258. }
  259. return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
  260. }
  261. static loadByEmail (email: string) {
  262. const query = {
  263. where: {
  264. email
  265. }
  266. }
  267. return UserModel.findOne(query)
  268. }
  269. static loadByUsernameOrEmail (username: string, email?: string) {
  270. if (!email) email = username
  271. const query = {
  272. where: {
  273. [ Sequelize.Op.or ]: [ { username }, { email } ]
  274. }
  275. }
  276. return UserModel.findOne(query)
  277. }
  278. static getOriginalVideoFileTotalFromUser (user: UserModel) {
  279. // Don't use sequelize because we need to use a sub query
  280. const query = UserModel.generateUserQuotaBaseSQL()
  281. return UserModel.getTotalRawQuery(query, user.id)
  282. }
  283. // Returns cumulative size of all video files uploaded in the last 24 hours.
  284. static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
  285. // Don't use sequelize because we need to use a sub query
  286. const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
  287. return UserModel.getTotalRawQuery(query, user.id)
  288. }
  289. static async getStats () {
  290. const totalUsers = await UserModel.count()
  291. return {
  292. totalUsers
  293. }
  294. }
  295. static autoComplete (search: string) {
  296. const query = {
  297. where: {
  298. username: {
  299. [ Sequelize.Op.like ]: `%${search}%`
  300. }
  301. },
  302. limit: 10
  303. }
  304. return UserModel.findAll(query)
  305. .then(u => u.map(u => u.username))
  306. }
  307. hasRight (right: UserRight) {
  308. return hasUserRight(this.role, right)
  309. }
  310. isPasswordMatch (password: string) {
  311. return comparePassword(password, this.password)
  312. }
  313. toFormattedJSON (): User {
  314. const videoQuotaUsed = this.get('videoQuotaUsed')
  315. const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
  316. const json = {
  317. id: this.id,
  318. username: this.username,
  319. email: this.email,
  320. emailVerified: this.emailVerified,
  321. nsfwPolicy: this.nsfwPolicy,
  322. webTorrentEnabled: this.webTorrentEnabled,
  323. videosHistoryEnabled: this.videosHistoryEnabled,
  324. autoPlayVideo: this.autoPlayVideo,
  325. role: this.role,
  326. roleLabel: USER_ROLE_LABELS[ this.role ],
  327. videoQuota: this.videoQuota,
  328. videoQuotaDaily: this.videoQuotaDaily,
  329. createdAt: this.createdAt,
  330. blocked: this.blocked,
  331. blockedReason: this.blockedReason,
  332. account: this.Account.toFormattedJSON(),
  333. videoChannels: [],
  334. videoQuotaUsed: videoQuotaUsed !== undefined
  335. ? parseInt(videoQuotaUsed, 10)
  336. : undefined,
  337. videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
  338. ? parseInt(videoQuotaUsedDaily, 10)
  339. : undefined
  340. }
  341. if (Array.isArray(this.Account.VideoChannels) === true) {
  342. json.videoChannels = this.Account.VideoChannels
  343. .map(c => c.toFormattedJSON())
  344. .sort((v1, v2) => {
  345. if (v1.createdAt < v2.createdAt) return -1
  346. if (v1.createdAt === v2.createdAt) return 0
  347. return 1
  348. })
  349. }
  350. return json
  351. }
  352. async isAbleToUploadVideo (videoFile: { size: number }) {
  353. if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
  354. const [ totalBytes, totalBytesDaily ] = await Promise.all([
  355. UserModel.getOriginalVideoFileTotalFromUser(this),
  356. UserModel.getOriginalVideoFileTotalDailyFromUser(this)
  357. ])
  358. const uploadedTotal = videoFile.size + totalBytes
  359. const uploadedDaily = videoFile.size + totalBytesDaily
  360. if (this.videoQuotaDaily === -1) {
  361. return uploadedTotal < this.videoQuota
  362. }
  363. if (this.videoQuota === -1) {
  364. return uploadedDaily < this.videoQuotaDaily
  365. }
  366. return (uploadedTotal < this.videoQuota) &&
  367. (uploadedDaily < this.videoQuotaDaily)
  368. }
  369. private static generateUserQuotaBaseSQL (where?: string) {
  370. const andWhere = where ? 'AND ' + where : ''
  371. return 'SELECT SUM("size") AS "total" ' +
  372. 'FROM (' +
  373. 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
  374. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
  375. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  376. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  377. 'WHERE "account"."userId" = $userId ' + andWhere +
  378. 'GROUP BY "video"."id"' +
  379. ') t'
  380. }
  381. private static getTotalRawQuery (query: string, userId: number) {
  382. const options = {
  383. bind: { userId },
  384. type: Sequelize.QueryTypes.SELECT
  385. }
  386. return UserModel.sequelize.query(query, options)
  387. .then(([ { total } ]) => {
  388. if (total === null) return 0
  389. return parseInt(total, 10)
  390. })
  391. }
  392. }