]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/account/user.ts
Add messages about privacy concerns (P2P)
[github/Chocobozzz/PeerTube.git] / server / models / account / user.ts
index 1401762c5c4da81023a55c321a599b992940995d..74cf0f4a8f7b0217bdff9dadaebf9fb5cf28c0e8 100644 (file)
 import * as Sequelize from 'sequelize'
-
-import { getSort, addMethodsToModel } from '../utils'
 import {
-  cryptPassword,
-  comparePassword,
-  isUserPasswordValid,
-  isUserUsernameValid,
-  isUserDisplayNSFWValid,
-  isUserVideoQuotaValid,
-  isUserRoleValid
-} from '../../helpers'
-import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
-
+  AllowNull, BeforeCreate, BeforeUpdate, Column, CreatedAt, DataType, Default, DefaultScope, HasMany, HasOne, Is, IsEmail, Model,
+  Scopes, Table, UpdatedAt
+} from 'sequelize-typescript'
+import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
+import { User, UserRole } from '../../../shared/models/users'
 import {
-  UserInstance,
-  UserAttributes,
-
-  UserMethods
-} from './user-interface'
-
-let User: Sequelize.Model<UserInstance, UserAttributes>
-let isPasswordMatch: UserMethods.IsPasswordMatch
-let hasRight: UserMethods.HasRight
-let toFormattedJSON: UserMethods.ToFormattedJSON
-let countTotal: UserMethods.CountTotal
-let getByUsername: UserMethods.GetByUsername
-let listForApi: UserMethods.ListForApi
-let loadById: UserMethods.LoadById
-let loadByUsername: UserMethods.LoadByUsername
-let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
-let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
-let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
-
-export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
-  User = sequelize.define<UserInstance, UserAttributes>('User',
+  isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
+  isUserVideoQuotaValid
+} from '../../helpers/custom-validators/users'
+import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
+import { OAuthTokenModel } from '../oauth/oauth-token'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoChannelModel } from '../video/video-channel'
+import { AccountModel } from './account'
+
+@DefaultScope({
+  include: [
     {
-      password: {
-        type: DataTypes.STRING,
-        allowNull: false,
-        validate: {
-          passwordValid: value => {
-            const res = isUserPasswordValid(value)
-            if (res === false) throw new Error('Password not valid.')
-          }
-        }
-      },
-      username: {
-        type: DataTypes.STRING,
-        allowNull: false,
-        validate: {
-          usernameValid: value => {
-            const res = isUserUsernameValid(value)
-            if (res === false) throw new Error('Username not valid.')
-          }
-        }
-      },
-      email: {
-        type: DataTypes.STRING(400),
-        allowNull: false,
-        validate: {
-          isEmail: true
-        }
-      },
-      displayNSFW: {
-        type: DataTypes.BOOLEAN,
-        allowNull: false,
-        defaultValue: false,
-        validate: {
-          nsfwValid: value => {
-            const res = isUserDisplayNSFWValid(value)
-            if (res === false) throw new Error('Display NSFW is not valid.')
-          }
-        }
-      },
-      role: {
-        type: DataTypes.INTEGER,
-        allowNull: false,
-        validate: {
-          roleValid: value => {
-            const res = isUserRoleValid(value)
-            if (res === false) throw new Error('Role is not valid.')
-          }
-        }
-      },
-      videoQuota: {
-        type: DataTypes.BIGINT,
-        allowNull: false,
-        validate: {
-          videoQuotaValid: value => {
-            const res = isUserVideoQuotaValid(value)
-            if (res === false) throw new Error('Video quota is not valid.')
-          }
-        }
+      model: () => AccountModel,
+      required: true
+    }
+  ]
+})
+@Scopes({
+  withVideoChannel: {
+    include: [
+      {
+        model: () => AccountModel,
+        required: true,
+        include: [ () => VideoChannelModel ]
       }
+    ]
+  }
+})
+@Table({
+  tableName: 'user',
+  indexes: [
+    {
+      fields: [ 'username' ],
+      unique: true
     },
     {
-      indexes: [
-        {
-          fields: [ 'username' ],
-          unique: true
-        },
-        {
-          fields: [ 'email' ],
-          unique: true
-        }
-      ],
-      hooks: {
-        beforeCreate: beforeCreateOrUpdate,
-        beforeUpdate: beforeCreateOrUpdate
-      }
+      fields: [ 'email' ],
+      unique: true
     }
-  )
-
-  const classMethods = [
-    associate,
-
-    countTotal,
-    getByUsername,
-    listForApi,
-    loadById,
-    loadByUsername,
-    loadByUsernameAndPopulateChannels,
-    loadByUsernameOrEmail
-  ]
-  const instanceMethods = [
-    hasRight,
-    isPasswordMatch,
-    toFormattedJSON,
-    isAbleToUploadVideo
   ]
-  addMethodsToModel(User, classMethods, instanceMethods)
-
-  return User
-}
+})
+export class UserModel extends Model<UserModel> {
+
+  @AllowNull(false)
+  @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
+  @Column
+  password: string
+
+  @AllowNull(false)
+  @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
+  @Column
+  username: string
+
+  @AllowNull(false)
+  @IsEmail
+  @Column(DataType.STRING(400))
+  email: string
+
+  @AllowNull(false)
+  @Default(false)
+  @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
+  @Column
+  displayNSFW: boolean
+
+  @AllowNull(false)
+  @Default(true)
+  @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
+  @Column
+  autoPlayVideo: boolean
+
+  @AllowNull(false)
+  @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
+  @Column
+  role: number
+
+  @AllowNull(false)
+  @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
+  @Column(DataType.BIGINT)
+  videoQuota: number
+
+  @CreatedAt
+  createdAt: Date
+
+  @UpdatedAt
+  updatedAt: Date
+
+  @HasOne(() => AccountModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade',
+    hooks: true
+  })
+  Account: AccountModel
 
-function beforeCreateOrUpdate (user: UserInstance) {
-  if (user.changed('password')) {
-    return cryptPassword(user.password)
-      .then(hash => {
-        user.password = hash
-        return undefined
-      })
+  @HasMany(() => OAuthTokenModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade'
+  })
+  OAuthTokens: OAuthTokenModel[]
+
+  @BeforeCreate
+  @BeforeUpdate
+  static cryptPasswordIfNeeded (instance: UserModel) {
+    if (instance.changed('password')) {
+      return cryptPassword(instance.password)
+        .then(hash => {
+          instance.password = hash
+          return undefined
+        })
+    }
   }
-}
-
-// ------------------------------ METHODS ------------------------------
-
-hasRight = function (this: UserInstance, right: UserRight) {
-  return hasUserRight(this.role, right)
-}
-
-isPasswordMatch = function (this: UserInstance, password: string) {
-  return comparePassword(password, this.password)
-}
 
-toFormattedJSON = function (this: UserInstance) {
-  const json = {
-    id: this.id,
-    username: this.username,
-    email: this.email,
-    displayNSFW: this.displayNSFW,
-    role: this.role,
-    roleLabel: USER_ROLE_LABELS[this.role],
-    videoQuota: this.videoQuota,
-    createdAt: this.createdAt,
-    author: {
-      id: this.Account.id,
-      uuid: this.Account.uuid
-    }
+  static countTotal () {
+    return this.count()
   }
 
-  if (Array.isArray(this.Account.VideoChannels) === true) {
-    const videoChannels = this.Account.VideoChannels
-      .map(c => c.toFormattedJSON())
-      .sort((v1, v2) => {
-        if (v1.createdAt < v2.createdAt) return -1
-        if (v1.createdAt === v2.createdAt) return 0
+  static listForApi (start: number, count: number, sort: string) {
+    const query = {
+      offset: start,
+      limit: count,
+      order: getSort(sort)
+    }
 
-        return 1
+    return UserModel.findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return {
+          data: rows,
+          total: count
+        }
       })
-
-    json['videoChannels'] = videoChannels
   }
 
-  return json
-}
+  static listEmailsWithRight (right: UserRight) {
+    const roles = Object.keys(USER_ROLE_LABELS)
+      .map(k => parseInt(k, 10) as UserRole)
+      .filter(role => hasUserRight(role, right))
 
-isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
-  if (this.videoQuota === -1) return Promise.resolve(true)
+    console.log(roles)
 
-  return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
-    return (videoFile.size + totalBytes) < this.videoQuota
-  })
-}
+    const query = {
+      attribute: [ 'email' ],
+      where: {
+        role: {
+          [Sequelize.Op.in]: roles
+        }
+      }
+    }
 
-// ------------------------------ STATICS ------------------------------
+    return UserModel.unscoped()
+      .findAll(query)
+      .then(u => u.map(u => u.email))
+  }
 
-function associate (models) {
-  User.hasOne(models.Account, {
-    foreignKey: 'userId',
-    onDelete: 'cascade'
-  })
+  static loadById (id: number) {
+    return UserModel.findById(id)
+  }
 
-  User.hasMany(models.OAuthToken, {
-    foreignKey: 'userId',
-    onDelete: 'cascade'
-  })
-}
+  static loadByUsername (username: string) {
+    const query = {
+      where: {
+        username
+      }
+    }
 
-countTotal = function () {
-  return this.count()
-}
+    return UserModel.findOne(query)
+  }
 
-getByUsername = function (username: string) {
-  const query = {
-    where: {
-      username: username
-    },
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+  static loadByUsernameAndPopulateChannels (username: string) {
+    const query = {
+      where: {
+        username
+      }
+    }
+
+    return UserModel.scope('withVideoChannel').findOne(query)
   }
 
-  return User.findOne(query)
-}
+  static loadByEmail (email: string) {
+    const query = {
+      where: {
+        email
+      }
+    }
 
-listForApi = function (start: number, count: number, sort: string) {
-  const query = {
-    offset: start,
-    limit: count,
-    order: [ getSort(sort) ],
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+    return UserModel.findOne(query)
   }
 
-  return User.findAndCountAll(query).then(({ rows, count }) => {
-    return {
-      data: rows,
-      total: count
+  static loadByUsernameOrEmail (username: string, email?: string) {
+    if (!email) email = username
+
+    const query = {
+      where: {
+        [ Sequelize.Op.or ]: [ { username }, { email } ]
+      }
     }
-  })
-}
 
-loadById = function (id: number) {
-  const options = {
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+    return UserModel.findOne(query)
   }
 
-  return User.findById(id, options)
-}
+  static getOriginalVideoFileTotalFromUser (user: UserModel) {
+    // Don't use sequelize because we need to use a sub query
+    const query = 'SELECT SUM("size") AS "total" FROM ' +
+      '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
+      'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
+      'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+      'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+      'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
+      'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
+
+    const options = {
+      bind: { userId: user.id },
+      type: Sequelize.QueryTypes.SELECT
+    }
+    return UserModel.sequelize.query(query, options)
+      .then(([ { total } ]) => {
+        if (total === null) return 0
 
-loadByUsername = function (username: string) {
-  const query = {
-    where: {
-      username
-    },
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+        return parseInt(total, 10)
+      })
   }
 
-  return User.findOne(query)
-}
+  hasRight (right: UserRight) {
+    return hasUserRight(this.role, right)
+  }
 
-loadByUsernameAndPopulateChannels = function (username: string) {
-  const query = {
-    where: {
-      username
-    },
-    include: [
-      {
-        model: User['sequelize'].models.Account,
-        required: true,
-        include: [ User['sequelize'].models.VideoChannel ]
-      }
-    ]
+  isPasswordMatch (password: string) {
+    return comparePassword(password, this.password)
   }
 
-  return User.findOne(query)
-}
+  toFormattedJSON (): User {
+    const json = {
+      id: this.id,
+      username: this.username,
+      email: this.email,
+      displayNSFW: this.displayNSFW,
+      autoPlayVideo: this.autoPlayVideo,
+      role: this.role,
+      roleLabel: USER_ROLE_LABELS[ this.role ],
+      videoQuota: this.videoQuota,
+      createdAt: this.createdAt,
+      account: this.Account.toFormattedJSON(),
+      videoChannels: []
+    }
+
+    if (Array.isArray(this.Account.VideoChannels) === true) {
+      json.videoChannels = this.Account.VideoChannels
+        .map(c => c.toFormattedJSON())
+        .sort((v1, v2) => {
+          if (v1.createdAt < v2.createdAt) return -1
+          if (v1.createdAt === v2.createdAt) return 0
 
-loadByUsernameOrEmail = function (username: string, email: string) {
-  const query = {
-    include: [ { model: User['sequelize'].models.Account, required: true } ],
-    where: {
-      [Sequelize.Op.or]: [ { username }, { email } ]
+          return 1
+        })
     }
+
+    return json
   }
 
-  // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
-  return (User as any).findOne(query)
-}
+  isAbleToUploadVideo (videoFile: Express.Multer.File) {
+    if (this.videoQuota === -1) return Promise.resolve(true)
 
-// ---------------------------------------------------------------------------
-
-function getOriginalVideoFileTotalFromUser (user: UserInstance) {
-  // Don't use sequelize because we need to use a sub query
-  const query = 'SELECT SUM("size") AS "total" FROM ' +
-                '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
-                'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
-                'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
-                'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' +
-                'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
-                'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
-
-  const options = {
-    bind: { userId: user.id },
-    type: Sequelize.QueryTypes.SELECT
+    return UserModel.getOriginalVideoFileTotalFromUser(this)
+      .then(totalBytes => {
+        return (videoFile.size + totalBytes) < this.videoQuota
+      })
   }
-  return User['sequelize'].query(query, options).then(([ { total } ]) => {
-    if (total === null) return 0
-
-    return parseInt(total, 10)
-  })
 }