1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 19:42:24 +02:00

server/server -> server/core

This commit is contained in:
Chocobozzz 2023-10-04 15:13:25 +02:00
parent 114327d4ce
commit 5a3d0650c9
No known key found for this signature in database
GPG key ID: 583A612D890159BE
838 changed files with 111 additions and 111 deletions

View file

@ -0,0 +1,72 @@
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { CONFIG } from '../../../initializers/config.js'
import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
import { asyncMiddleware, buildRateLimiter } from '../../../middlewares/index.js'
import {
registrationVerifyEmailValidator,
usersAskSendVerifyEmailValidator,
usersVerifyEmailValidator
} from '../../../middlewares/validators/index.js'
const askSendEmailLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
})
const emailVerificationRouter = express.Router()
emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ],
askSendEmailLimiter,
asyncMiddleware(usersAskSendVerifyEmailValidator),
asyncMiddleware(reSendVerifyUserEmail)
)
emailVerificationRouter.post('/:id/verify-email',
asyncMiddleware(usersVerifyEmailValidator),
asyncMiddleware(verifyUserEmail)
)
emailVerificationRouter.post('/registrations/:registrationId/verify-email',
asyncMiddleware(registrationVerifyEmailValidator),
asyncMiddleware(verifyRegistrationEmail)
)
// ---------------------------------------------------------------------------
export {
emailVerificationRouter
}
async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
const user = res.locals.user
const registration = res.locals.userRegistration
if (user) await sendVerifyUserEmail(user)
else if (registration) await sendVerifyRegistrationEmail(registration)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function verifyUserEmail (req: express.Request, res: express.Response) {
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 verifyRegistrationEmail (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
registration.emailVerified = true
await registration.save()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}

View file

@ -0,0 +1,319 @@
import express from 'express'
import { tokensRouter } from '@server/controllers/api/users/token.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
import { MUserAccountDefault } from '@server/types/models/index.js'
import { pick } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@peertube/peertube-models'
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger.js'
import { logger } from '../../../helpers/logger.js'
import { generateRandomString, getFormattedObjects } from '../../../helpers/utils.js'
import { WEBSERVER } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { Emailer } from '../../../lib/emailer.js'
import { Redis } from '../../../lib/redis.js'
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user.js'
import {
adminUsersSortValidator,
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userAutocompleteValidator,
usersAddValidator,
usersGetValidator,
usersListValidator,
usersRemoveValidator,
usersUpdateValidator
} from '../../../middlewares/index.js'
import {
ensureCanModerateUser,
usersAskResetPasswordValidator,
usersBlockingValidator,
usersResetPasswordValidator
} from '../../../middlewares/validators/index.js'
import { UserModel } from '../../../models/user/user.js'
import { emailVerificationRouter } from './email-verification.js'
import { meRouter } from './me.js'
import { myAbusesRouter } from './my-abuses.js'
import { myBlocklistRouter } from './my-blocklist.js'
import { myVideosHistoryRouter } from './my-history.js'
import { myNotificationsRouter } from './my-notifications.js'
import { mySubscriptionsRouter } from './my-subscriptions.js'
import { myVideoPlaylistsRouter } from './my-video-playlists.js'
import { registrationsRouter } from './registrations.js'
import { twoFactorRouter } from './two-factor.js'
const auditLogger = auditLoggerFactory('users')
const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
usersRouter.use('/', emailVerificationRouter)
usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter)
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,
adminUsersSortValidator,
setDefaultSort,
setDefaultPagination,
usersListValidator,
asyncMiddleware(listUsers)
)
usersRouter.post('/:id/block',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersBlockingValidator),
ensureCanModerateUser,
asyncMiddleware(blockUser)
)
usersRouter.post('/:id/unblock',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersBlockingValidator),
ensureCanModerateUser,
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.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersUpdateValidator),
ensureCanModerateUser,
asyncMiddleware(updateUser)
)
usersRouter.delete('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersRemoveValidator),
ensureCanModerateUser,
asyncMiddleware(removeUser)
)
usersRouter.post('/ask-reset-password',
asyncMiddleware(usersAskResetPasswordValidator),
asyncMiddleware(askResetUserPassword)
)
usersRouter.post('/:id/reset-password',
asyncMiddleware(usersResetPasswordValidator),
asyncMiddleware(resetUserPassword)
)
// ---------------------------------------------------------------------------
export {
usersRouter
}
// ---------------------------------------------------------------------------
async function createUser (req: express.Request, res: express.Response) {
const body: UserCreate = req.body
const userToCreate = buildUser({
...pick(body, [ 'username', 'password', 'email', 'role', 'videoQuota', 'videoQuotaDaily', 'adminFlags' ]),
emailVerified: null
})
// NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail.
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) {
// this will send an email for newly created users, so then can set their first password.
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 unblockUser (req: express.Request, res: express.Response) {
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: express.Request, res: express.Response) {
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: express.Request, res: express.Response) {
return res.json(res.locals.user.toFormattedJSON({ withAdminFlags: true }))
}
async function autocompleteUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.autoComplete(req.query.search as string)
return res.json(resultList)
}
async function listUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.listForAdminApi({
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: express.Request, res: express.Response) {
const user = res.locals.user
auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
await sequelizeTypescript.transaction(async t => {
// Use a transaction to avoid inconsistencies with hooks (account/channel deletion & federation)
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: express.Request, res: express.Response) {
const body: UserUpdate = req.body
const userToUpdate = res.locals.user
const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
const keysToUpdate: (keyof UserUpdate)[] = [
'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()
// Destroy user token to refresh rights
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 })
// Don't need to send this update to followers, these attributes are not federated
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function askResetUserPassword (req: express.Request, res: express.Response) {
const user = res.locals.user
const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function resetUserPassword (req: express.Request, res: express.Response) {
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 changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
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 })
})
Emailer.Instance.addUserBlockJob(user, block, reason)
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
}

View file

@ -0,0 +1,283 @@
import 'multer'
import express from 'express'
import { pick } from '@peertube/peertube-core-utils'
import {
ActorImageType,
HttpStatusCode,
UserUpdateMe,
UserVideoQuota,
UserVideoRate as FormattedUserVideoRate
} from '@peertube/peertube-models'
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { CONFIG } from '../../../initializers/config.js'
import { MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
usersUpdateMeValidator,
usersVideoRatingValidator
} from '../../../middlewares/index.js'
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image.js'
import {
deleteMeValidator,
getMyVideoImportsValidator,
usersVideosValidator,
videoImportsSortValidator,
videosSortValidator
} from '../../../middlewares/validators/index.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { AccountModel } from '../../../models/account/account.js'
import { UserModel } from '../../../models/user/user.js'
import { VideoImportModel } from '../../../models/video/video-import.js'
import { VideoModel } from '../../../models/video/video.js'
const auditLogger = auditLoggerFactory('users')
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
const meRouter = express.Router()
meRouter.get('/me',
authenticate,
asyncMiddleware(getUserInformation)
)
meRouter.delete('/me',
authenticate,
deleteMeValidator,
asyncMiddleware(deleteMe)
)
meRouter.get('/me/video-quota-used',
authenticate,
asyncMiddleware(getUserVideoQuotaUsed)
)
meRouter.get('/me/videos/imports',
authenticate,
paginationValidator,
videoImportsSortValidator,
setDefaultSort,
setDefaultPagination,
getMyVideoImportsValidator,
asyncMiddleware(getUserVideoImports)
)
meRouter.get('/me/videos',
authenticate,
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
asyncMiddleware(usersVideosValidator),
asyncMiddleware(getUserVideos)
)
meRouter.get('/me/videos/:videoId/rating',
authenticate,
asyncMiddleware(usersVideoRatingValidator),
asyncMiddleware(getUserVideoRating)
)
meRouter.put('/me',
authenticate,
asyncMiddleware(usersUpdateMeValidator),
asyncRetryTransactionMiddleware(updateMe)
)
meRouter.post('/me/avatar/pick',
authenticate,
reqAvatarFile,
updateAvatarValidator,
asyncRetryTransactionMiddleware(updateMyAvatar)
)
meRouter.delete('/me/avatar',
authenticate,
asyncRetryTransactionMiddleware(deleteMyAvatar)
)
// ---------------------------------------------------------------------------
export {
meRouter
}
// ---------------------------------------------------------------------------
async function getUserVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const apiOptions = await Hooks.wrapObject({
accountId: user.Account.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
channelId: res.locals.videoChannel?.id,
isLive: req.query.isLive
}, 'filter:api.user.me.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listUserVideosForApi,
apiOptions,
'filter:api.user.me.videos.list.result'
)
const additionalAttributes = {
waitTranscoding: true,
state: true,
scheduledUpdate: true,
blacklistInfo: true
}
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
}
async function getUserVideoImports (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await VideoImportModel.listUserVideoImportsForApi({
userId: user.id,
...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort', 'search', 'videoChannelSyncId' ])
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserInformation (req: express.Request, res: express.Response) {
// We did not load channels in res.locals.user
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
return res.json(user.toMeFormattedJSON())
}
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
const data: UserVideoQuota = {
videoQuotaUsed,
videoQuotaUsedDaily
}
return res.json(data)
}
async function getUserVideoRating (req: express.Request, res: express.Response) {
const videoId = res.locals.videoId.id
const accountId = +res.locals.oauth.token.User.Account.id
const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null)
const rating = ratingObj ? ratingObj.type : 'none'
const json: FormattedUserVideoRate = {
videoId,
rating
}
return res.json(json)
}
async function deleteMe (req: express.Request, res: express.Response) {
const user = await UserModel.loadByIdWithChannels(res.locals.oauth.token.User.id)
auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
await user.destroy()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateMe (req: express.Request, res: express.Response) {
const body: UserUpdateMe = req.body
let sendVerificationEmail = false
const user = res.locals.oauth.token.user
const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [
'password',
'nsfwPolicy',
'p2pEnabled',
'autoPlayVideo',
'autoPlayNextVideo',
'autoPlayNextVideoPlaylist',
'videosHistoryEnabled',
'videoLanguages',
'theme',
'noInstanceConfigWarningModal',
'noAccountSetupWarningModal',
'noWelcomeModal',
'emailPublic',
'p2pEnabled'
]
for (const key of keysToUpdate) {
if (body[key] !== undefined) user.set(key, body[key])
}
if (body.email !== undefined) {
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
user.pendingEmail = body.email
sendVerificationEmail = true
} else {
user.email = body.email
}
}
await sequelizeTypescript.transaction(async t => {
await user.save({ transaction: t })
if (body.displayName === undefined && body.description === undefined) return
const userAccount = await AccountModel.load(user.Account.id, t)
if (body.displayName !== undefined) userAccount.name = body.displayName
if (body.description !== undefined) userAccount.description = body.description
await userAccount.save({ transaction: t })
await sendUpdateActor(userAccount, t)
})
if (sendVerificationEmail === true) {
await sendVerifyUserEmail(user, true)
}
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateMyAvatar (req: express.Request, res: express.Response) {
const avatarPhysicalFile = req.files['avatarfile'][0]
const user = res.locals.oauth.token.user
const userAccount = await AccountModel.load(user.Account.id)
const avatars = await updateLocalActorImageFiles(
userAccount,
avatarPhysicalFile,
ActorImageType.AVATAR
)
return res.json({
avatars: avatars.map(avatar => avatar.toFormattedJSON())
})
}
async function deleteMyAvatar (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const userAccount = await AccountModel.load(user.Account.id)
await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
return res.json({ avatars: [] })
}

View file

@ -0,0 +1,48 @@
import express from 'express'
import { AbuseModel } from '@server/models/abuse/abuse.js'
import {
abuseListForUserValidator,
abusesSortValidator,
asyncMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
const myAbusesRouter = express.Router()
myAbusesRouter.get('/me/abuses',
authenticate,
paginationValidator,
abusesSortValidator,
setDefaultSort,
setDefaultPagination,
abuseListForUserValidator,
asyncMiddleware(listMyAbuses)
)
// ---------------------------------------------------------------------------
export {
myAbusesRouter
}
// ---------------------------------------------------------------------------
async function listMyAbuses (req: express.Request, res: express.Response) {
const resultList = await AbuseModel.listForUserApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
id: req.query.id,
search: req.query.search,
state: req.query.state,
user: res.locals.oauth.token.User
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedUserJSON())
})
}

View file

@ -0,0 +1,154 @@
import 'multer'
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
addAccountInBlocklist,
addServerInBlocklist,
removeAccountFromBlocklist,
removeServerFromBlocklist
} from '../../../lib/blocklist.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
unblockAccountByAccountValidator
} from '../../../middlewares/index.js'
import {
accountsBlocklistSortValidator,
blockAccountValidator,
blockServerValidator,
serversBlocklistSortValidator,
unblockServerByAccountValidator
} from '../../../middlewares/validators/index.js'
import { AccountBlocklistModel } from '../../../models/account/account-blocklist.js'
import { ServerBlocklistModel } from '../../../models/server/server-blocklist.js'
const myBlocklistRouter = express.Router()
myBlocklistRouter.get('/me/blocklist/accounts',
authenticate,
paginationValidator,
accountsBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedAccounts)
)
myBlocklistRouter.post('/me/blocklist/accounts',
authenticate,
asyncMiddleware(blockAccountValidator),
asyncRetryTransactionMiddleware(blockAccount)
)
myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
authenticate,
asyncMiddleware(unblockAccountByAccountValidator),
asyncRetryTransactionMiddleware(unblockAccount)
)
myBlocklistRouter.get('/me/blocklist/servers',
authenticate,
paginationValidator,
serversBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedServers)
)
myBlocklistRouter.post('/me/blocklist/servers',
authenticate,
asyncMiddleware(blockServerValidator),
asyncRetryTransactionMiddleware(blockServer)
)
myBlocklistRouter.delete('/me/blocklist/servers/:host',
authenticate,
asyncMiddleware(unblockServerByAccountValidator),
asyncRetryTransactionMiddleware(unblockServer)
)
export {
myBlocklistRouter
}
// ---------------------------------------------------------------------------
async function listBlockedAccounts (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await AccountBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: user.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockAccount (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const accountToBlock = res.locals.account
await addAccountInBlocklist(user.Account.id, accountToBlock.id)
UserNotificationModel.removeNotificationsOf({
id: accountToBlock.id,
type: 'account',
forUserId: user.id
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function unblockAccount (req: express.Request, res: express.Response) {
const accountBlock = res.locals.accountBlock
await removeAccountFromBlocklist(accountBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function listBlockedServers (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await ServerBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: user.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockServer (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const serverToBlock = res.locals.server
await addServerInBlocklist(user.Account.id, serverToBlock.id)
UserNotificationModel.removeNotificationsOf({
id: serverToBlock.id,
type: 'server',
forUserId: user.id
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function unblockServer (req: express.Request, res: express.Response) {
const serverBlock = res.locals.serverBlock
await removeServerFromBlocklist(serverBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}

View file

@ -0,0 +1,75 @@
import express from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
userHistoryListValidator,
userHistoryRemoveAllValidator,
userHistoryRemoveElementValidator
} from '../../../middlewares/index.js'
import { UserVideoHistoryModel } from '../../../models/user/user-video-history.js'
const myVideosHistoryRouter = express.Router()
myVideosHistoryRouter.get('/me/history/videos',
authenticate,
paginationValidator,
setDefaultPagination,
userHistoryListValidator,
asyncMiddleware(listMyVideosHistory)
)
myVideosHistoryRouter.delete('/me/history/videos/:videoId',
authenticate,
userHistoryRemoveElementValidator,
asyncMiddleware(removeUserHistoryElement)
)
myVideosHistoryRouter.post('/me/history/videos/remove',
authenticate,
userHistoryRemoveAllValidator,
asyncRetryTransactionMiddleware(removeAllUserHistory)
)
// ---------------------------------------------------------------------------
export {
myVideosHistoryRouter
}
// ---------------------------------------------------------------------------
async function listMyVideosHistory (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count, req.query.search)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function removeUserHistoryElement (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserVideoHistoryModel.removeUserHistoryElement(user, forceNumber(req.params.videoId))
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeAllUserHistory (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const beforeDate = req.body.beforeDate || null
await sequelizeTypescript.transaction(t => {
return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t)
})
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}

View file

@ -0,0 +1,115 @@
import 'multer'
import express from 'express'
import { HttpStatusCode, UserNotificationSetting } from '@peertube/peertube-models'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userNotificationsSortValidator
} from '../../../middlewares/index.js'
import {
listUserNotificationsValidator,
markAsReadUserNotificationsValidator,
updateNotificationSettingsValidator
} from '../../../middlewares/validators/user-notifications.js'
import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting.js'
import { meRouter } from './me.js'
const myNotificationsRouter = express.Router()
meRouter.put('/me/notification-settings',
authenticate,
updateNotificationSettingsValidator,
asyncRetryTransactionMiddleware(updateNotificationSettings)
)
myNotificationsRouter.get('/me/notifications',
authenticate,
paginationValidator,
userNotificationsSortValidator,
setDefaultSort,
setDefaultPagination,
listUserNotificationsValidator,
asyncMiddleware(listUserNotifications)
)
myNotificationsRouter.post('/me/notifications/read',
authenticate,
markAsReadUserNotificationsValidator,
asyncMiddleware(markAsReadUserNotifications)
)
myNotificationsRouter.post('/me/notifications/read-all',
authenticate,
asyncMiddleware(markAsReadAllUserNotifications)
)
export {
myNotificationsRouter
}
// ---------------------------------------------------------------------------
async function updateNotificationSettings (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const body = req.body as UserNotificationSetting
const query = {
where: {
userId: user.id
}
}
const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
abuseAsModerator: body.abuseAsModerator,
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished,
myVideoImportFinished: body.myVideoImportFinished,
newFollow: body.newFollow,
newUserRegistration: body.newUserRegistration,
commentMention: body.commentMention,
newInstanceFollower: body.newInstanceFollower,
autoInstanceFollowing: body.autoInstanceFollowing,
abuseNewMessage: body.abuseNewMessage,
abuseStateChange: body.abuseStateChange,
newPeerTubeVersion: body.newPeerTubeVersion,
newPluginVersion: body.newPluginVersion,
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
}
await UserNotificationSettingModel.update(values, query)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function listUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function markAsReadUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserNotificationModel.markAsRead(user.id, req.body.ids)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserNotificationModel.markAllAsRead(user.id)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}

View file

@ -0,0 +1,193 @@
import 'multer'
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { handlesToNameAndHost } from '@server/helpers/actors.js'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { sendUndoFollow } from '@server/lib/activitypub/send/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { JobQueue } from '../../../lib/job-queue/index.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
userSubscriptionAddValidator,
userSubscriptionGetValidator
} from '../../../middlewares/index.js'
import {
areSubscriptionsExistValidator,
userSubscriptionListValidator,
userSubscriptionsSortValidator,
videosSortValidator
} from '../../../middlewares/validators/index.js'
import { ActorFollowModel } from '../../../models/actor/actor-follow.js'
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
import { VideoModel } from '../../../models/video/video.js'
const mySubscriptionsRouter = express.Router()
mySubscriptionsRouter.get('/me/subscriptions/videos',
authenticate,
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
commonVideosFiltersValidator,
asyncMiddleware(getUserSubscriptionVideos)
)
mySubscriptionsRouter.get('/me/subscriptions/exist',
authenticate,
areSubscriptionsExistValidator,
asyncMiddleware(areSubscriptionsExist)
)
mySubscriptionsRouter.get('/me/subscriptions',
authenticate,
paginationValidator,
userSubscriptionsSortValidator,
setDefaultSort,
setDefaultPagination,
userSubscriptionListValidator,
asyncMiddleware(getUserSubscriptions)
)
mySubscriptionsRouter.post('/me/subscriptions',
authenticate,
userSubscriptionAddValidator,
addUserSubscription
)
mySubscriptionsRouter.get('/me/subscriptions/:uri',
authenticate,
userSubscriptionGetValidator,
asyncMiddleware(getUserSubscription)
)
mySubscriptionsRouter.delete('/me/subscriptions/:uri',
authenticate,
userSubscriptionGetValidator,
asyncRetryTransactionMiddleware(deleteUserSubscription)
)
// ---------------------------------------------------------------------------
export {
mySubscriptionsRouter
}
// ---------------------------------------------------------------------------
async function areSubscriptionsExist (req: express.Request, res: express.Response) {
const uris = req.query.uris as string[]
const user = res.locals.oauth.token.User
const sanitizedHandles = handlesToNameAndHost(uris)
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
const existObject: { [id: string ]: boolean } = {}
for (const sanitizedHandle of sanitizedHandles) {
const obj = results.find(r => {
const server = r.ActorFollowing.Server
return r.ActorFollowing.preferredUsername.toLowerCase() === sanitizedHandle.name.toLowerCase() &&
(
(!server && !sanitizedHandle.host) ||
(server.host === sanitizedHandle.host)
)
})
existObject[sanitizedHandle.handle] = obj !== undefined
}
return res.json(existObject)
}
function addUserSubscription (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const [ name, host ] = req.body.uri.split('@')
const payload = {
name,
host,
assertIsChannel: true,
followerActorId: user.Account.Actor.id
}
JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function getUserSubscription (req: express.Request, res: express.Response) {
const subscription = res.locals.subscription
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
return res.json(videoChannel.toFormattedJSON())
}
async function deleteUserSubscription (req: express.Request, res: express.Response) {
const subscription = res.locals.subscription
await sequelizeTypescript.transaction(async t => {
if (subscription.state === 'accepted') {
sendUndoFollow(subscription, t)
}
return subscription.destroy({ transaction: t })
})
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
async function getUserSubscriptions (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const actorId = user.Account.Actor.id
const resultList = await ActorFollowModel.listSubscriptionsForApi({
actorId,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower: {
actorId: user.Account.Actor.id,
orLocalVideos: false
},
nsfw: buildNSFWFilter(res, query.nsfw),
user,
countVideos
}, 'filter:api.user.me.subscription-videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi,
apiOptions,
'filter:api.user.me.subscription-videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}

View file

@ -0,0 +1,51 @@
import express from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideosExistInPlaylists } from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { asyncMiddleware, authenticate } from '../../../middlewares/index.js'
import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists.js'
import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
const myVideoPlaylistsRouter = express.Router()
myVideoPlaylistsRouter.get('/me/video-playlists/videos-exist',
authenticate,
doVideosInPlaylistExistValidator,
asyncMiddleware(doVideosInPlaylistExist)
)
// ---------------------------------------------------------------------------
export {
myVideoPlaylistsRouter
}
// ---------------------------------------------------------------------------
async function doVideosInPlaylistExist (req: express.Request, res: express.Response) {
const videoIds = req.query.videoIds.map(i => forceNumber(i))
const user = res.locals.oauth.token.User
const results = await VideoPlaylistModel.listPlaylistSummariesOf(user.Account.id, videoIds)
const existObject: VideosExistInPlaylists = {}
for (const videoId of videoIds) {
existObject[videoId] = []
}
for (const result of results) {
for (const element of result.VideoPlaylistElements) {
existObject[element.videoId].push({
playlistElementId: element.id,
playlistId: result.id,
playlistDisplayName: result.name,
playlistShortUUID: uuidToShort(result.uuid),
startTimestamp: element.startTimestamp,
stopTimestamp: element.stopTimestamp
})
}
}
return res.json(existObject)
}

View file

@ -0,0 +1,249 @@
import express from 'express'
import { Emailer } from '@server/lib/emailer.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
import { pick } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
UserRegister,
UserRegistrationRequest,
UserRegistrationState,
UserRegistrationUpdateState,
UserRight
} from '@peertube/peertube-models'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger.js'
import { logger } from '../../../helpers/logger.js'
import { CONFIG } from '../../../initializers/config.js'
import { Notifier } from '../../../lib/notifier/index.js'
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
import {
acceptOrRejectRegistrationValidator,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
buildRateLimiter,
ensureUserHasRight,
ensureUserRegistrationAllowedFactory,
ensureUserRegistrationAllowedForIP,
getRegistrationValidator,
listRegistrationsValidator,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userRegistrationsSortValidator,
usersDirectRegistrationValidator,
usersRequestRegistrationValidator
} from '../../../middlewares/index.js'
const auditLogger = auditLoggerFactory('users')
const registrationRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
skipFailedRequests: true
})
const registrationsRouter = express.Router()
registrationsRouter.post('/registrations/request',
registrationRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersRequestRegistrationValidator),
asyncRetryTransactionMiddleware(requestRegistration)
)
registrationsRouter.post('/registrations/:registrationId/accept',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(acceptOrRejectRegistrationValidator),
asyncRetryTransactionMiddleware(acceptRegistration)
)
registrationsRouter.post('/registrations/:registrationId/reject',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(acceptOrRejectRegistrationValidator),
asyncRetryTransactionMiddleware(rejectRegistration)
)
registrationsRouter.delete('/registrations/:registrationId',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(getRegistrationValidator),
asyncRetryTransactionMiddleware(deleteRegistration)
)
registrationsRouter.get('/registrations',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
paginationValidator,
userRegistrationsSortValidator,
setDefaultSort,
setDefaultPagination,
listRegistrationsValidator,
asyncMiddleware(listRegistrations)
)
registrationsRouter.post('/register',
registrationRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersDirectRegistrationValidator),
asyncRetryTransactionMiddleware(registerUser)
)
// ---------------------------------------------------------------------------
export {
registrationsRouter
}
// ---------------------------------------------------------------------------
async function requestRegistration (req: express.Request, res: express.Response) {
const body: UserRegistrationRequest = req.body
const registration = new UserRegistrationModel({
...pick(body, [ 'username', 'password', 'email', 'registrationReason' ]),
accountDisplayName: body.displayName,
channelDisplayName: body.channel?.displayName,
channelHandle: body.channel?.name,
state: UserRegistrationState.PENDING,
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
await registration.save()
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
await sendVerifyRegistrationEmail(registration)
}
Notifier.Instance.notifyOnNewRegistrationRequest(registration)
Hooks.runAction('action:api.user.requested-registration', { body, registration, req, res })
return res.json(registration.toFormattedJSON())
}
// ---------------------------------------------------------------------------
async function acceptRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
const userToCreate = buildUser({
username: registration.username,
password: registration.password,
email: registration.email,
emailVerified: registration.emailVerified
})
// We already encrypted password in registration model
userToCreate.skipPasswordEncryption = true
// TODO: handle conflicts if someone else created a channel handle/user handle/user email between registration and approval
const { user } = await createUserAccountAndChannelAndPlaylist({
userToCreate,
userDisplayName: registration.accountDisplayName,
channelNames: registration.channelHandle && registration.channelDisplayName
? {
name: registration.channelHandle,
displayName: registration.channelDisplayName
}
: undefined
})
registration.userId = user.id
registration.state = UserRegistrationState.ACCEPTED
registration.moderationResponse = body.moderationResponse
await registration.save()
logger.info('Registration of %s accepted', registration.username)
if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function rejectRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
registration.state = UserRegistrationState.REJECTED
registration.moderationResponse = body.moderationResponse
await registration.save()
if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
logger.info('Registration of %s rejected', registration.username)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
async function deleteRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
await registration.destroy()
logger.info('Registration of %s deleted', registration.username)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
async function listRegistrations (req: express.Request, res: express.Response) {
const resultList = await UserRegistrationModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedJSON())
})
}
// ---------------------------------------------------------------------------
async function registerUser (req: express.Request, res: express.Response) {
const body: UserRegister = req.body
const userToCreate = buildUser({
...pick(body, [ 'username', 'password', 'email' ]),
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({
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.notifyOnNewDirectRegistration(user)
Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View file

@ -0,0 +1,131 @@
import express from 'express'
import { ScopedToken } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { OTP } from '@server/initializers/constants.js'
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model.js'
import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares/index.js'
import { buildUUID } from '@peertube/peertube-node-utils'
const tokensRouter = express.Router()
const loginRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
max: CONFIG.RATES_LIMIT.LOGIN.MAX
})
tokensRouter.post('/token',
loginRateLimiter,
openapiOperationDoc({ operationId: 'getOAuthToken' }),
asyncMiddleware(handleToken)
)
tokensRouter.post('/revoke-token',
openapiOperationDoc({ operationId: 'revokeOAuthToken' }),
authenticate,
asyncMiddleware(handleTokenRevocation)
)
tokensRouter.get('/scoped-tokens',
authenticate,
getScopedTokens
)
tokensRouter.post('/scoped-tokens',
authenticate,
asyncMiddleware(renewScopedTokens)
)
// ---------------------------------------------------------------------------
export {
tokensRouter
}
// ---------------------------------------------------------------------------
async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
const grantType = req.body.grant_type
try {
const bypassLogin = await buildByPassLogin(req, grantType)
const refreshTokenAuthName = grantType === 'refresh_token'
? await getAuthNameFromRefreshGrant(req.body.refresh_token)
: undefined
const options = {
refreshTokenAuthName,
bypassLogin
}
const token = await handleOAuthToken(req, options)
res.set('Cache-Control', 'no-store')
res.set('Pragma', 'no-cache')
Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip, req, res })
return res.json({
token_type: 'Bearer',
access_token: token.accessToken,
refresh_token: token.refreshToken,
expires_in: token.accessTokenExpiresIn,
refresh_token_expires_in: token.refreshTokenExpiresIn
})
} catch (err) {
logger.warn('Login error', { err })
if (err instanceof MissingTwoFactorError) {
res.set(OTP.HEADER_NAME, OTP.HEADER_REQUIRED_VALUE)
}
return res.fail({
status: err.code,
message: err.message,
type: err.name
})
}
}
async function handleTokenRevocation (req: express.Request, res: express.Response) {
const token = res.locals.oauth.token
const result = await revokeToken(token, { req, explicitLogout: true })
return res.json(result)
}
function getScopedTokens (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
return res.json({
feedToken: user.feedToken
} as ScopedToken)
}
async function renewScopedTokens (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
user.feedToken = buildUUID()
await user.save()
return res.json({
feedToken: user.feedToken
} as ScopedToken)
}
async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
if (grantType !== 'password') return undefined
if (req.body.externalAuthToken) {
// Consistency with the getBypassFromPasswordGrant promise
return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
}
return getBypassFromPasswordGrant(req.body.username, req.body.password)
}

View file

@ -0,0 +1,95 @@
import express from 'express'
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp.js'
import { encrypt } from '@server/helpers/peertube-crypto.js'
import { CONFIG } from '@server/initializers/config.js'
import { Redis } from '@server/lib/redis.js'
import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares/index.js'
import {
confirmTwoFactorValidator,
disableTwoFactorValidator,
requestOrConfirmTwoFactorValidator
} from '@server/middlewares/validators/two-factor.js'
import { HttpStatusCode, TwoFactorEnableResult } from '@peertube/peertube-models'
const twoFactorRouter = express.Router()
twoFactorRouter.post('/:id/two-factor/request',
authenticate,
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(requestOrConfirmTwoFactorValidator),
asyncMiddleware(requestTwoFactor)
)
twoFactorRouter.post('/:id/two-factor/confirm-request',
authenticate,
asyncMiddleware(requestOrConfirmTwoFactorValidator),
confirmTwoFactorValidator,
asyncMiddleware(confirmRequestTwoFactor)
)
twoFactorRouter.post('/:id/two-factor/disable',
authenticate,
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(disableTwoFactorValidator),
asyncMiddleware(disableTwoFactor)
)
// ---------------------------------------------------------------------------
export {
twoFactorRouter
}
// ---------------------------------------------------------------------------
async function requestTwoFactor (req: express.Request, res: express.Response) {
const user = res.locals.user
const { secret, uri } = generateOTPSecret(user.email)
const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE)
const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret)
return res.json({
otpRequest: {
requestToken,
secret,
uri
}
} as TwoFactorEnableResult)
}
async function confirmRequestTwoFactor (req: express.Request, res: express.Response) {
const requestToken = req.body.requestToken
const otpToken = req.body.otpToken
const user = res.locals.user
const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
if (!encryptedSecret) {
return res.fail({
message: 'Invalid request token',
status: HttpStatusCode.FORBIDDEN_403
})
}
if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) {
return res.fail({
message: 'Invalid OTP token',
status: HttpStatusCode.FORBIDDEN_403
})
}
user.otpSecret = encryptedSecret
await user.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function disableTwoFactor (req: express.Request, res: express.Response) {
const user = res.locals.user
user.otpSecret = null
await user.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}