import express from 'express' ; import RateLimit from 'express-rate-limit' ; import { tokensRouter } from '@server/controllers/api/users/token' ; import { Hooks } from '@server/lib/plugins/hooks' ; import { OAuthTokenModel } from '@server/models/oauth/oauth-token' ; import { MUser , MUserAccountDefault } from '@server/types/models' ; import { HttpStatusCode , UserAdminFlag , UserCreate , UserCreateResult , UserRegister , UserRight , UserRole , UserUpdate } from '@shared/models' ; import { auditLoggerFactory , getAuditIdFromRes , UserAuditView } from '../../../helpers/audit-logger' ; import { logger } from '../../../helpers/logger' ; import { generateRandomString , getFormattedObjects } from '../../../helpers/utils' ; import { CONFIG } from '../../../initializers/config' ; import { WEBSERVER } from '../../../initializers/constants' ; import { sequelizeTypescript } from '../../../initializers/database' ; import { Emailer } from '../../../lib/emailer' ; import { Notifier } from '../../../lib/notifier' ; import { Redis } from '../../../lib/redis' ; import { createUserAccountAndChannelAndPlaylist , sendVerifyUserEmail } from '../../../lib/user' ; import { asyncMiddleware , asyncRetryTransactionMiddleware , authenticate , ensureUserHasRight , ensureUserRegistrationAllowed , ensureUserRegistrationAllowedForIP , paginationValidator , setDefaultPagination , setDefaultSort , userAutocompleteValidator , usersAddValidator , usersGetValidator , usersListValidator , usersRegisterValidator , usersRemoveValidator , usersSortValidator , usersUpdateValidator } from '../../../middlewares' ; import { ensureCanManageUser , usersAskResetPasswordValidator , usersAskSendVerifyEmailValidator , usersBlockingValidator , usersResetPasswordValidator , usersVerifyEmailValidator } from '../../../middlewares/validators' ; import { UserModel } from '../../../models/user/user' ; import { meRouter } from './me' ; import { myAbusesRouter } from './my-abuses' ; import { myBlocklistRouter } from './my-blocklist' ; import { myVideosHistoryRouter } from './my-history' ; import { myNotificationsRouter } from './my-notifications' ; import { mySubscriptionsRouter } from './my-subscriptions' ; import { myVideoPlaylistsRouter } from './my-video-playlists' ; const auditLogger = auditLoggerFactory ( 'users' ) ; const signupRateLimiter = RateLimit ( { windowMs : CONFIG . RATES_LIMIT . SIGNUP . WINDOW_MS , max : CONFIG . RATES_LIMIT . SIGNUP . MAX , skipFailedRequests : true } ) ; const askSendEmailLimiter = RateLimit ( { windowMs : CONFIG . RATES_LIMIT . ASK_SEND_EMAIL . WINDOW_MS , max : CONFIG . RATES_LIMIT . ASK_SEND_EMAIL . MAX } ) ; const usersRouter = express . Router ( ) ; usersRouter . use ( '/' , tokensRouter ) ; usersRouter . use ( '/' , myNotificationsRouter ) ; usersRouter . use ( '/' , mySubscriptionsRouter ) ; usersRouter . use ( '/' , myBlocklistRouter ) ; usersRouter . use ( '/' , myVideosHistoryRouter ) ; usersRouter . use ( '/' , myVideoPlaylistsRouter ) ; usersRouter . use ( '/' , myAbusesRouter ) ; usersRouter . use ( '/' , meRouter ) ; usersRouter . get ( '/autocomplete' , userAutocompleteValidator , asyncMiddleware ( autocompleteUsers ) ) ; usersRouter . get ( '/' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , paginationValidator , usersSortValidator , setDefaultSort , setDefaultPagination , usersListValidator , asyncMiddleware ( listUsers ) ) ; usersRouter . post ( '/:id/block' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersBlockingValidator ) , ensureCanManageUser , asyncMiddleware ( blockUser ) ) ; usersRouter . post ( '/:id/unblock' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersBlockingValidator ) , ensureCanManageUser , asyncMiddleware ( unblockUser ) ) ; usersRouter . get ( '/:id' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersGetValidator ) , getUser ) ; usersRouter . post ( '/' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersAddValidator ) , asyncRetryTransactionMiddleware ( createUser ) ) ; usersRouter . post ( '/register' , signupRateLimiter , asyncMiddleware ( ensureUserRegistrationAllowed ) , ensureUserRegistrationAllowedForIP , asyncMiddleware ( usersRegisterValidator ) , asyncRetryTransactionMiddleware ( registerUser ) ) ; usersRouter . put ( '/:id' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersUpdateValidator ) , ensureCanManageUser , asyncMiddleware ( updateUser ) ) ; usersRouter . delete ( '/:id' , authenticate , ensureUserHasRight ( UserRight . MANAGE_USERS ) , asyncMiddleware ( usersRemoveValidator ) , ensureCanManageUser , asyncMiddleware ( removeUser ) ) ; usersRouter . post ( '/ask-reset-password' , asyncMiddleware ( usersAskResetPasswordValidator ) , asyncMiddleware ( askResetUserPassword ) ) ; usersRouter . post ( '/:id/reset-password' , asyncMiddleware ( usersResetPasswordValidator ) , asyncMiddleware ( resetUserPassword ) ) ; usersRouter . post ( '/ask-send-verify-email' , askSendEmailLimiter , asyncMiddleware ( usersAskSendVerifyEmailValidator ) , asyncMiddleware ( reSendVerifyUserEmail ) ) ; usersRouter . post ( '/:id/verify-email' , asyncMiddleware ( usersVerifyEmailValidator ) , asyncMiddleware ( verifyUserEmail ) ) ; export { usersRouter } ; async function createUser ( req , res ) { const body = req . body ; const userToCreate = new UserModel ( { username : body . username , password : body . password , email : body . email , nsfwPolicy : CONFIG . INSTANCE . DEFAULT_NSFW_POLICY , p2pEnabled : CONFIG . DEFAULTS . P2P . WEBAPP . ENABLED , autoPlayVideo : true , role : body . role , videoQuota : body . videoQuota , videoQuotaDaily : body . videoQuotaDaily , adminFlags : body . adminFlags || UserAdminFlag . NONE } ) as MUser ; const createPassword = userToCreate . password === '' ; if ( createPassword ) { userToCreate . password = await generateRandomString ( 20 ) ; } const { user , account , videoChannel } = await createUserAccountAndChannelAndPlaylist ( { userToCreate , channelNames : body . channelName && { name : body . channelName , displayName : body . channelName } } ) ; auditLogger . create ( getAuditIdFromRes ( res ) , new UserAuditView ( user . toFormattedJSON ( ) ) ) ; logger . info ( 'User %s with its channel and account created.' , body . username ) ; if ( createPassword ) { logger . info ( 'Sending to user %s a create password email' , body . username ) ; const verificationString = await Redis . Instance . setCreatePasswordVerificationString ( user . id ) ; const url = WEBSERVER . URL + '/reset-password?userId=' + user . id + '&verificationString=' + verificationString ; Emailer . Instance . addPasswordCreateEmailJob ( userToCreate . username , user . email , url ) ; } Hooks . runAction ( 'action:api.user.created' , { body , user , account , videoChannel , req , res } ) ; return res . json ( { user : { id : user . id , account : { id : account . id } } as UserCreateResult } ) ; } async function registerUser ( req , res ) { const body = req . body ; const userToCreate = new UserModel ( { username : body . username , password : body . password , email : body . email , nsfwPolicy : CONFIG . INSTANCE . DEFAULT_NSFW_POLICY , p2pEnabled : CONFIG . DEFAULTS . P2P . WEBAPP . ENABLED , autoPlayVideo : true , role : UserRole . USER , videoQuota : CONFIG . USER . VIDEO_QUOTA , videoQuotaDaily : CONFIG . USER . VIDEO_QUOTA_DAILY , emailVerified : CONFIG . SIGNUP . REQUIRES_EMAIL_VERIFICATION ? false : null } ) ; const { user , account , videoChannel } = await createUserAccountAndChannelAndPlaylist ( { userToCreate : userToCreate , userDisplayName : body . displayName || undefined , channelNames : body . channel } ) ; auditLogger . create ( body . username , new UserAuditView ( user . toFormattedJSON ( ) ) ) ; logger . info ( 'User %s with its channel and account registered.' , body . username ) ; if ( CONFIG . SIGNUP . REQUIRES_EMAIL_VERIFICATION ) { await sendVerifyUserEmail ( user ) ; } Notifier . Instance . notifyOnNewUserRegistration ( user ) ; Hooks . runAction ( 'action:api.user.registered' , { body , user , account , videoChannel , req , res } ) ; return res . type ( 'json' ) . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function unblockUser ( req , res ) { const user = res . locals . user ; await changeUserBlock ( res , user , false ) ; Hooks . runAction ( 'action:api.user.unblocked' , { user , req , res } ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function blockUser ( req , res ) { const user = res . locals . user ; const reason = req . body . reason ; await changeUserBlock ( res , user , true , reason ) ; Hooks . runAction ( 'action:api.user.blocked' , { user , req , res } ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } function getUser ( req , res ) { return res . json ( res . locals . user . toFormattedJSON ( { withAdminFlags : true } ) ) ; } async function autocompleteUsers ( req , res ) { const resultList = await UserModel . autoComplete ( req . query . search as string ) ; return res . json ( resultList ) ; } async function listUsers ( req , res ) { const resultList = await UserModel . listForApi ( { start : req . query . start , count : req . query . count , sort : req . query . sort , search : req . query . search , blocked : req . query . blocked } ) ; return res . json ( getFormattedObjects ( resultList . data , resultList . total , { withAdminFlags : true } ) ) ; } async function removeUser ( req , res ) { const user = res . locals . user ; auditLogger . delete ( getAuditIdFromRes ( res ) , new UserAuditView ( user . toFormattedJSON ( ) ) ) ; await sequelizeTypescript . transaction ( async ( t ) => { await user . destroy ( { transaction : t } ) ; } ) ; Hooks . runAction ( 'action:api.user.deleted' , { user , req , res } ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function updateUser ( req , res ) { const body = req . body ; const userToUpdate = res . locals . user ; const oldUserAuditView = new UserAuditView ( userToUpdate . toFormattedJSON ( ) ) ; const roleChanged = body . role !== undefined && body . role !== userToUpdate . role ; const keysToUpdate = [ 'password' , 'email' , 'emailVerified' , 'videoQuota' , 'videoQuotaDaily' , 'role' , 'adminFlags' , 'pluginAuth' ] ; for ( const key of keysToUpdate ) { if ( body [ key ] !== undefined ) userToUpdate . set ( key , body [ key ] ) ; } const user = await userToUpdate . save ( ) ; if ( roleChanged || body . password !== undefined ) await OAuthTokenModel . deleteUserToken ( userToUpdate . id ) ; auditLogger . update ( getAuditIdFromRes ( res ) , new UserAuditView ( user . toFormattedJSON ( ) ) , oldUserAuditView ) ; Hooks . runAction ( 'action:api.user.updated' , { user , req , res } ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function askResetUserPassword ( req , res ) { const user = res . locals . user ; const verificationString = await Redis . Instance . setResetPasswordVerificationString ( user . id ) ; const url = WEBSERVER . URL + '/reset-password?userId=' + user . id + '&verificationString=' + verificationString ; await Emailer . Instance . addPasswordResetEmailJob ( user . username , user . email , url ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function resetUserPassword ( req , res ) { const user = res . locals . user ; user . password = req . body . password ; await user . save ( ) ; await Redis . Instance . removePasswordVerificationString ( user . id ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function reSendVerifyUserEmail ( req , res ) { const user = res . locals . user ; await sendVerifyUserEmail ( user ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function verifyUserEmail ( req , res ) { const user = res . locals . user ; user . emailVerified = true ; if ( req . body . isPendingEmail === true ) { user . email = user . pendingEmail ; user . pendingEmail = null ; } await user . save ( ) ; return res . status ( HttpStatusCode . NO_CONTENT_204 ) . end ( ) ; } async function changeUserBlock ( res , user , block , reason ? ) { const oldUserAuditView = new UserAuditView ( user . toFormattedJSON ( ) ) ; user . blocked = block ; user . blockedReason = reason || null ; await sequelizeTypescript . transaction ( async ( t ) => { await OAuthTokenModel . deleteUserToken ( user . id , t ) ; await user . save ( { transaction : t } ) ; } ) ; await Emailer . Instance . addUserBlockJob ( user , block , reason ) ; auditLogger . update ( getAuditIdFromRes ( res ) , new UserAuditView ( user . toFormattedJSON ( ) ) , oldUserAuditView ) ; }