user.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. import { FindOptions, literal, Op, QueryTypes } 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. isUserAdminFlagsValid,
  26. isUserAutoPlayVideoValid,
  27. isUserBlockedReasonValid,
  28. isUserBlockedValid,
  29. isUserEmailVerifiedValid,
  30. isUserNSFWPolicyValid,
  31. isUserPasswordValid,
  32. isUserRoleValid,
  33. isUserUsernameValid,
  34. isUserVideoLanguages,
  35. isUserVideoQuotaDailyValid,
  36. isUserVideoQuotaValid,
  37. isUserVideosHistoryEnabledValid,
  38. isUserWebTorrentEnabledValid
  39. } from '../../helpers/custom-validators/users'
  40. import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
  41. import { OAuthTokenModel } from '../oauth/oauth-token'
  42. import { getSort, throwIfNotValid } from '../utils'
  43. import { VideoChannelModel } from '../video/video-channel'
  44. import { AccountModel } from './account'
  45. import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
  46. import { values } from 'lodash'
  47. import { NSFW_POLICY_TYPES } from '../../initializers/constants'
  48. import { clearCacheByUserId } from '../../lib/oauth-model'
  49. import { UserNotificationSettingModel } from './user-notification-setting'
  50. import { VideoModel } from '../video/video'
  51. import { ActorModel } from '../activitypub/actor'
  52. import { ActorFollowModel } from '../activitypub/actor-follow'
  53. import { VideoImportModel } from '../video/video-import'
  54. import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
  55. enum ScopeNames {
  56. WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
  57. }
  58. @DefaultScope(() => ({
  59. include: [
  60. {
  61. model: AccountModel,
  62. required: true
  63. },
  64. {
  65. model: UserNotificationSettingModel,
  66. required: true
  67. }
  68. ]
  69. }))
  70. @Scopes(() => ({
  71. [ScopeNames.WITH_VIDEO_CHANNEL]: {
  72. include: [
  73. {
  74. model: AccountModel,
  75. required: true,
  76. include: [ VideoChannelModel ]
  77. },
  78. {
  79. model: UserNotificationSettingModel,
  80. required: true
  81. }
  82. ]
  83. }
  84. }))
  85. @Table({
  86. tableName: 'user',
  87. indexes: [
  88. {
  89. fields: [ 'username' ],
  90. unique: true
  91. },
  92. {
  93. fields: [ 'email' ],
  94. unique: true
  95. }
  96. ]
  97. })
  98. export class UserModel extends Model<UserModel> {
  99. @AllowNull(false)
  100. @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
  101. @Column
  102. password: string
  103. @AllowNull(false)
  104. @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
  105. @Column
  106. username: string
  107. @AllowNull(false)
  108. @IsEmail
  109. @Column(DataType.STRING(400))
  110. email: string
  111. @AllowNull(true)
  112. @IsEmail
  113. @Column(DataType.STRING(400))
  114. pendingEmail: string
  115. @AllowNull(true)
  116. @Default(null)
  117. @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
  118. @Column
  119. emailVerified: boolean
  120. @AllowNull(false)
  121. @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
  122. @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
  123. nsfwPolicy: NSFWPolicyType
  124. @AllowNull(false)
  125. @Default(true)
  126. @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
  127. @Column
  128. webTorrentEnabled: boolean
  129. @AllowNull(false)
  130. @Default(true)
  131. @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
  132. @Column
  133. videosHistoryEnabled: boolean
  134. @AllowNull(false)
  135. @Default(true)
  136. @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
  137. @Column
  138. autoPlayVideo: boolean
  139. @AllowNull(true)
  140. @Default(null)
  141. @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
  142. @Column(DataType.ARRAY(DataType.STRING))
  143. videoLanguages: string[]
  144. @AllowNull(false)
  145. @Default(UserAdminFlag.NONE)
  146. @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
  147. @Column
  148. adminFlags?: UserAdminFlag
  149. @AllowNull(false)
  150. @Default(false)
  151. @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
  152. @Column
  153. blocked: boolean
  154. @AllowNull(true)
  155. @Default(null)
  156. @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
  157. @Column
  158. blockedReason: string
  159. @AllowNull(false)
  160. @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
  161. @Column
  162. role: number
  163. @AllowNull(false)
  164. @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
  165. @Column(DataType.BIGINT)
  166. videoQuota: number
  167. @AllowNull(false)
  168. @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
  169. @Column(DataType.BIGINT)
  170. videoQuotaDaily: number
  171. @CreatedAt
  172. createdAt: Date
  173. @UpdatedAt
  174. updatedAt: Date
  175. @HasOne(() => AccountModel, {
  176. foreignKey: 'userId',
  177. onDelete: 'cascade',
  178. hooks: true
  179. })
  180. Account: AccountModel
  181. @HasOne(() => UserNotificationSettingModel, {
  182. foreignKey: 'userId',
  183. onDelete: 'cascade',
  184. hooks: true
  185. })
  186. NotificationSetting: UserNotificationSettingModel
  187. @HasMany(() => VideoImportModel, {
  188. foreignKey: 'userId',
  189. onDelete: 'cascade'
  190. })
  191. VideoImports: VideoImportModel[]
  192. @HasMany(() => OAuthTokenModel, {
  193. foreignKey: 'userId',
  194. onDelete: 'cascade'
  195. })
  196. OAuthTokens: OAuthTokenModel[]
  197. @BeforeCreate
  198. @BeforeUpdate
  199. static cryptPasswordIfNeeded (instance: UserModel) {
  200. if (instance.changed('password')) {
  201. return cryptPassword(instance.password)
  202. .then(hash => {
  203. instance.password = hash
  204. return undefined
  205. })
  206. }
  207. }
  208. @AfterUpdate
  209. @AfterDestroy
  210. static removeTokenCache (instance: UserModel) {
  211. return clearCacheByUserId(instance.id)
  212. }
  213. static countTotal () {
  214. return this.count()
  215. }
  216. static listForApi (start: number, count: number, sort: string, search?: string) {
  217. let where = undefined
  218. if (search) {
  219. where = {
  220. [Op.or]: [
  221. {
  222. email: {
  223. [Op.iLike]: '%' + search + '%'
  224. }
  225. },
  226. {
  227. username: {
  228. [ Op.iLike ]: '%' + search + '%'
  229. }
  230. }
  231. ]
  232. }
  233. }
  234. const query: FindOptions = {
  235. attributes: {
  236. include: [
  237. [
  238. literal(
  239. '(' +
  240. 'SELECT COALESCE(SUM("size"), 0) ' +
  241. 'FROM (' +
  242. 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
  243. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
  244. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  245. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  246. 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
  247. ') t' +
  248. ')'
  249. ),
  250. 'videoQuotaUsed'
  251. ]
  252. ]
  253. },
  254. offset: start,
  255. limit: count,
  256. order: getSort(sort),
  257. where
  258. }
  259. return UserModel.findAndCountAll(query)
  260. .then(({ rows, count }) => {
  261. return {
  262. data: rows,
  263. total: count
  264. }
  265. })
  266. }
  267. static listWithRight (right: UserRight) {
  268. const roles = Object.keys(USER_ROLE_LABELS)
  269. .map(k => parseInt(k, 10) as UserRole)
  270. .filter(role => hasUserRight(role, right))
  271. const query = {
  272. where: {
  273. role: {
  274. [Op.in]: roles
  275. }
  276. }
  277. }
  278. return UserModel.findAll(query)
  279. }
  280. static listUserSubscribersOf (actorId: number) {
  281. const query = {
  282. include: [
  283. {
  284. model: UserNotificationSettingModel.unscoped(),
  285. required: true
  286. },
  287. {
  288. attributes: [ 'userId' ],
  289. model: AccountModel.unscoped(),
  290. required: true,
  291. include: [
  292. {
  293. attributes: [ ],
  294. model: ActorModel.unscoped(),
  295. required: true,
  296. where: {
  297. serverId: null
  298. },
  299. include: [
  300. {
  301. attributes: [ ],
  302. as: 'ActorFollowings',
  303. model: ActorFollowModel.unscoped(),
  304. required: true,
  305. where: {
  306. targetActorId: actorId
  307. }
  308. }
  309. ]
  310. }
  311. ]
  312. }
  313. ]
  314. }
  315. return UserModel.unscoped().findAll(query)
  316. }
  317. static listByUsernames (usernames: string[]) {
  318. const query = {
  319. where: {
  320. username: usernames
  321. }
  322. }
  323. return UserModel.findAll(query)
  324. }
  325. static loadById (id: number) {
  326. return UserModel.findByPk(id)
  327. }
  328. static loadByUsername (username: string) {
  329. const query = {
  330. where: {
  331. username
  332. }
  333. }
  334. return UserModel.findOne(query)
  335. }
  336. static loadByUsernameAndPopulateChannels (username: string) {
  337. const query = {
  338. where: {
  339. username
  340. }
  341. }
  342. return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
  343. }
  344. static loadByEmail (email: string) {
  345. const query = {
  346. where: {
  347. email
  348. }
  349. }
  350. return UserModel.findOne(query)
  351. }
  352. static loadByUsernameOrEmail (username: string, email?: string) {
  353. if (!email) email = username
  354. const query = {
  355. where: {
  356. [ Op.or ]: [ { username }, { email } ]
  357. }
  358. }
  359. return UserModel.findOne(query)
  360. }
  361. static loadByVideoId (videoId: number) {
  362. const query = {
  363. include: [
  364. {
  365. required: true,
  366. attributes: [ 'id' ],
  367. model: AccountModel.unscoped(),
  368. include: [
  369. {
  370. required: true,
  371. attributes: [ 'id' ],
  372. model: VideoChannelModel.unscoped(),
  373. include: [
  374. {
  375. required: true,
  376. attributes: [ 'id' ],
  377. model: VideoModel.unscoped(),
  378. where: {
  379. id: videoId
  380. }
  381. }
  382. ]
  383. }
  384. ]
  385. }
  386. ]
  387. }
  388. return UserModel.findOne(query)
  389. }
  390. static loadByVideoImportId (videoImportId: number) {
  391. const query = {
  392. include: [
  393. {
  394. required: true,
  395. attributes: [ 'id' ],
  396. model: VideoImportModel.unscoped(),
  397. where: {
  398. id: videoImportId
  399. }
  400. }
  401. ]
  402. }
  403. return UserModel.findOne(query)
  404. }
  405. static loadByChannelActorId (videoChannelActorId: number) {
  406. const query = {
  407. include: [
  408. {
  409. required: true,
  410. attributes: [ 'id' ],
  411. model: AccountModel.unscoped(),
  412. include: [
  413. {
  414. required: true,
  415. attributes: [ 'id' ],
  416. model: VideoChannelModel.unscoped(),
  417. where: {
  418. actorId: videoChannelActorId
  419. }
  420. }
  421. ]
  422. }
  423. ]
  424. }
  425. return UserModel.findOne(query)
  426. }
  427. static loadByAccountActorId (accountActorId: number) {
  428. const query = {
  429. include: [
  430. {
  431. required: true,
  432. attributes: [ 'id' ],
  433. model: AccountModel.unscoped(),
  434. where: {
  435. actorId: accountActorId
  436. }
  437. }
  438. ]
  439. }
  440. return UserModel.findOne(query)
  441. }
  442. static getOriginalVideoFileTotalFromUser (user: UserModel) {
  443. // Don't use sequelize because we need to use a sub query
  444. const query = UserModel.generateUserQuotaBaseSQL()
  445. return UserModel.getTotalRawQuery(query, user.id)
  446. }
  447. // Returns cumulative size of all video files uploaded in the last 24 hours.
  448. static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
  449. // Don't use sequelize because we need to use a sub query
  450. const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
  451. return UserModel.getTotalRawQuery(query, user.id)
  452. }
  453. static async getStats () {
  454. const totalUsers = await UserModel.count()
  455. return {
  456. totalUsers
  457. }
  458. }
  459. static autoComplete (search: string) {
  460. const query = {
  461. where: {
  462. username: {
  463. [ Op.like ]: `%${search}%`
  464. }
  465. },
  466. limit: 10
  467. }
  468. return UserModel.findAll(query)
  469. .then(u => u.map(u => u.username))
  470. }
  471. hasRight (right: UserRight) {
  472. return hasUserRight(this.role, right)
  473. }
  474. hasAdminFlag (flag: UserAdminFlag) {
  475. return this.adminFlags & flag
  476. }
  477. isPasswordMatch (password: string) {
  478. return comparePassword(password, this.password)
  479. }
  480. toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User {
  481. const videoQuotaUsed = this.get('videoQuotaUsed')
  482. const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
  483. const json = {
  484. id: this.id,
  485. username: this.username,
  486. email: this.email,
  487. pendingEmail: this.pendingEmail,
  488. emailVerified: this.emailVerified,
  489. nsfwPolicy: this.nsfwPolicy,
  490. webTorrentEnabled: this.webTorrentEnabled,
  491. videosHistoryEnabled: this.videosHistoryEnabled,
  492. autoPlayVideo: this.autoPlayVideo,
  493. videoLanguages: this.videoLanguages,
  494. role: this.role,
  495. roleLabel: USER_ROLE_LABELS[ this.role ],
  496. videoQuota: this.videoQuota,
  497. videoQuotaDaily: this.videoQuotaDaily,
  498. createdAt: this.createdAt,
  499. blocked: this.blocked,
  500. blockedReason: this.blockedReason,
  501. account: this.Account.toFormattedJSON(),
  502. notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
  503. videoChannels: [],
  504. videoQuotaUsed: videoQuotaUsed !== undefined
  505. ? parseInt(videoQuotaUsed + '', 10)
  506. : undefined,
  507. videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
  508. ? parseInt(videoQuotaUsedDaily + '', 10)
  509. : undefined
  510. }
  511. if (parameters.withAdminFlags) {
  512. Object.assign(json, { adminFlags: this.adminFlags })
  513. }
  514. if (Array.isArray(this.Account.VideoChannels) === true) {
  515. json.videoChannels = this.Account.VideoChannels
  516. .map(c => c.toFormattedJSON())
  517. .sort((v1, v2) => {
  518. if (v1.createdAt < v2.createdAt) return -1
  519. if (v1.createdAt === v2.createdAt) return 0
  520. return 1
  521. })
  522. }
  523. return json
  524. }
  525. async isAbleToUploadVideo (videoFile: { size: number }) {
  526. if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
  527. const [ totalBytes, totalBytesDaily ] = await Promise.all([
  528. UserModel.getOriginalVideoFileTotalFromUser(this),
  529. UserModel.getOriginalVideoFileTotalDailyFromUser(this)
  530. ])
  531. const uploadedTotal = videoFile.size + totalBytes
  532. const uploadedDaily = videoFile.size + totalBytesDaily
  533. if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
  534. if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
  535. return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
  536. }
  537. private static generateUserQuotaBaseSQL (where?: string) {
  538. const andWhere = where ? 'AND ' + where : ''
  539. return 'SELECT SUM("size") AS "total" ' +
  540. 'FROM (' +
  541. 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
  542. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
  543. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  544. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  545. 'WHERE "account"."userId" = $userId ' + andWhere +
  546. 'GROUP BY "video"."id"' +
  547. ') t'
  548. }
  549. private static getTotalRawQuery (query: string, userId: number) {
  550. const options = {
  551. bind: { userId },
  552. type: QueryTypes.SELECT as QueryTypes.SELECT
  553. }
  554. return UserModel.sequelize.query<{ total: string }>(query, options)
  555. .then(([ { total } ]) => {
  556. if (total === null) return 0
  557. return parseInt(total, 10)
  558. })
  559. }
  560. }