DANGER ZONE
@@ -213,7 +213,7 @@
-
+
+
+
+
+
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
index 68fa1215f..698628149 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
@@ -48,17 +48,13 @@ my-user-real-quota-info {
}
.danger-zone {
- .reset-password-email {
- margin-bottom: 30px;
+ button {
+ @include peertube-button;
+ @include danger-button;
+ @include disable-outline;
- button {
- @include peertube-button;
- @include danger-button;
- @include disable-outline;
-
- display: block;
- margin-top: 0;
- }
+ display: block;
+ margin-top: 0;
}
}
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
index 6dae4110d..21e9629ab 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
@@ -60,10 +60,22 @@ export abstract class UserEdit extends FormReactive implements OnInit {
]
}
+ displayDangerZone () {
+ if (this.isCreation()) return false
+ if (this.user?.pluginAuth) return false
+ if (this.auth.getUser().id === this.user.id) return false
+
+ return true
+ }
+
resetPassword () {
return
}
+ disableTwoFactorAuth () {
+ return
+ }
+
getUserVideoQuota () {
return this.form.value['videoQuota']
}
diff --git a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
index bab288a67..1482a1902 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
@@ -10,7 +10,7 @@ import {
USER_VIDEO_QUOTA_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
-import { UserAdminService } from '@app/shared/shared-users'
+import { TwoFactorService, UserAdminService } from '@app/shared/shared-users'
import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
import { UserEdit } from './user-edit'
@@ -34,6 +34,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
private router: Router,
private notifier: Notifier,
private userService: UserService,
+ private twoFactorService: TwoFactorService,
private userAdminService: UserAdminService
) {
super()
@@ -120,12 +121,24 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`)
},
- error: err => {
- this.error = err.message
- }
+ error: err => this.notifier.error(err.message)
})
}
+ disableTwoFactorAuth () {
+ this.twoFactorService.disableTwoFactor({ userId: this.user.id })
+ .subscribe({
+ next: () => {
+ this.user.twoFactorEnabled = false
+
+ this.notifier.success($localize`Two factor authentication of ${this.user.username} disabled.`)
+ },
+
+ error: err => this.notifier.error(err.message)
+ })
+
+ }
+
private onUserFetched (userJson: UserType) {
this.user = new User(userJson)
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
index ef83009a5..cc774bde3 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
@@ -1,3 +1,2 @@
export * from './my-account-two-factor-button.component'
export * from './my-account-two-factor.component'
-export * from './two-factor.service'
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
index 03b00e933..97ffb6013 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
@@ -1,7 +1,7 @@
import { Subject } from 'rxjs'
import { Component, Input, OnInit } from '@angular/core'
import { AuthService, ConfirmService, Notifier, User } from '@app/core'
-import { TwoFactorService } from './two-factor.service'
+import { TwoFactorService } from '@app/shared/shared-users'
@Component({
selector: 'my-account-two-factor-button',
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
index e4d4188f7..259090d64 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
@@ -4,7 +4,7 @@ import { Router } from '@angular/router'
import { AuthService, Notifier, User } from '@app/core'
import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactiveService } from '@app/shared/shared-forms'
-import { TwoFactorService } from './two-factor.service'
+import { TwoFactorService } from '@app/shared/shared-users'
@Component({
selector: 'my-account-two-factor',
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index f5beaa4db..84b057647 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -11,6 +11,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation'
import { SharedShareModal } from '@app/shared/shared-share-modal'
import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
+import { SharedUsersModule } from '@app/shared/shared-users'
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
@@ -24,11 +25,7 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
-import {
- MyAccountTwoFactorButtonComponent,
- MyAccountTwoFactorComponent,
- TwoFactorService
-} from './my-account-settings/my-account-two-factor'
+import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
import { MyAccountComponent } from './my-account.component'
@NgModule({
@@ -44,6 +41,7 @@ import { MyAccountComponent } from './my-account.component'
SharedFormModule,
SharedModerationModule,
SharedUserInterfaceSettingsModule,
+ SharedUsersModule,
SharedGlobalIconModule,
SharedAbuseListModule,
SharedShareModal,
@@ -74,9 +72,7 @@ import { MyAccountComponent } from './my-account.component'
MyAccountComponent
],
- providers: [
- TwoFactorService
- ]
+ providers: []
})
export class MyAccountModule {
}
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts
index 8f90f2515..20e60486d 100644
--- a/client/src/app/shared/shared-users/index.ts
+++ b/client/src/app/shared/shared-users/index.ts
@@ -1,4 +1,5 @@
export * from './user-admin.service'
export * from './user-signup.service'
+export * from './two-factor.service'
export * from './shared-users.module'
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts
index 2a1dadf20..5a1675dc9 100644
--- a/client/src/app/shared/shared-users/shared-users.module.ts
+++ b/client/src/app/shared/shared-users/shared-users.module.ts
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core'
import { SharedMainModule } from '../shared-main/shared-main.module'
+import { TwoFactorService } from './two-factor.service'
import { UserAdminService } from './user-admin.service'
import { UserSignupService } from './user-signup.service'
@@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service'
providers: [
UserSignupService,
- UserAdminService
+ UserAdminService,
+ TwoFactorService
]
})
export class SharedUsersModule { }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts b/client/src/app/shared/shared-users/two-factor.service.ts
similarity index 98%
rename from client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts
rename to client/src/app/shared/shared-users/two-factor.service.ts
index c0e5ac492..9ff916f15 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts
+++ b/client/src/app/shared/shared-users/two-factor.service.ts
@@ -40,7 +40,7 @@ export class TwoFactorService {
disableTwoFactor (options: {
userId: number
- currentPassword: string
+ currentPassword?: string
}) {
const { userId, currentPassword } = options
diff --git a/server/controllers/api/users/two-factor.ts b/server/controllers/api/users/two-factor.ts
index 1725294e7..79f63a62d 100644
--- a/server/controllers/api/users/two-factor.ts
+++ b/server/controllers/api/users/two-factor.ts
@@ -1,7 +1,7 @@
import express from 'express'
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
import { Redis } from '@server/lib/redis'
-import { asyncMiddleware, authenticate, usersCheckCurrentPassword } from '@server/middlewares'
+import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
import {
confirmTwoFactorValidator,
disableTwoFactorValidator,
@@ -13,7 +13,7 @@ const twoFactorRouter = express.Router()
twoFactorRouter.post('/:id/two-factor/request',
authenticate,
- asyncMiddleware(usersCheckCurrentPassword),
+ asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(requestOrConfirmTwoFactorValidator),
asyncMiddleware(requestTwoFactor)
)
@@ -27,7 +27,7 @@ twoFactorRouter.post('/:id/two-factor/confirm-request',
twoFactorRouter.post('/:id/two-factor/disable',
authenticate,
- asyncMiddleware(usersCheckCurrentPassword),
+ asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(disableTwoFactorValidator),
asyncMiddleware(disableTwoFactor)
)
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 8aca50900..dcf47ce76 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -24,6 +24,8 @@ function createPrivateAndPublicKeys () {
// User password checks
function comparePassword (plainPassword: string, hashPassword: string) {
+ if (!plainPassword) return Promise.resolve(false)
+
return bcryptComparePromise(plainPassword, hashPassword)
}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 046029547..055af3b64 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -506,23 +506,40 @@ const usersVerifyEmailValidator = [
}
]
-const usersCheckCurrentPassword = [
- body('currentPassword').custom(exists),
+const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
+ return [
+ body('currentPassword').optional().custom(exists),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res)) return
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
- const user = res.locals.oauth.token.User
- if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
- return res.fail({
- status: HttpStatusCode.FORBIDDEN_403,
- message: 'currentPassword is invalid.'
- })
+ const user = res.locals.oauth.token.User
+ const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
+ const targetUserId = parseInt(targetUserIdGetter(req) + '')
+
+ // Admin/moderator action on another user, skip the password check
+ if (isAdminOrModerator && targetUserId !== user.id) {
+ return next()
+ }
+
+ if (!req.body.currentPassword) {
+ return res.fail({
+ status: HttpStatusCode.BAD_REQUEST_400,
+ message: 'currentPassword is missing'
+ })
+ }
+
+ if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'currentPassword is invalid.'
+ })
+ }
+
+ return next()
}
-
- return next()
- }
-]
+ ]
+}
const userAutocompleteValidator = [
param('search')
@@ -591,7 +608,7 @@ export {
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
- usersCheckCurrentPassword,
+ usersCheckCurrentPasswordFactory,
ensureUserRegistrationAllowed,
ensureUserRegistrationAllowedForIP,
usersGetValidator,
diff --git a/server/tests/api/check-params/two-factor.ts b/server/tests/api/check-params/two-factor.ts
index e7ca5490c..f8365f1b5 100644
--- a/server/tests/api/check-params/two-factor.ts
+++ b/server/tests/api/check-params/two-factor.ts
@@ -86,6 +86,15 @@ describe('Test two factor API validators', function () {
})
})
+ it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () {
+ await server.twoFactor.request({ userId })
+ })
+
+ it('Should fail to request two factor without a password when targeting myself with an admin account', async function () {
+ await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+ })
+
it('Should succeed to request my two factor auth', async function () {
{
const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
@@ -234,7 +243,7 @@ describe('Test two factor API validators', function () {
})
})
- it('Should fail to disabled two factor with an incorrect password', async function () {
+ it('Should fail to disable two factor with an incorrect password', async function () {
await server.twoFactor.disable({
userId,
token: userToken,
@@ -243,16 +252,20 @@ describe('Test two factor API validators', function () {
})
})
+ it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () {
+ await server.twoFactor.disable({ userId })
+ await server.twoFactor.requestAndConfirm({ userId })
+ })
+
+ it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () {
+ await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+ })
+
it('Should succeed to disable another user two factor with the appropriate rights', async function () {
await server.twoFactor.disable({ userId, currentPassword: rootPassword })
- // Reinit
- const { otpRequest } = await server.twoFactor.request({ userId, currentPassword: rootPassword })
- await server.twoFactor.confirmRequest({
- userId,
- requestToken: otpRequest.requestToken,
- otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
- })
+ await server.twoFactor.requestAndConfirm({ userId })
})
it('Should succeed to update my two factor auth', async function () {
diff --git a/server/tests/api/users/two-factor.ts b/server/tests/api/users/two-factor.ts
index 450aac4dc..0dcab9e17 100644
--- a/server/tests/api/users/two-factor.ts
+++ b/server/tests/api/users/two-factor.ts
@@ -7,13 +7,14 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ
async function login (options: {
server: PeerTubeServer
- password?: string
+ username: string
+ password: string
otpToken?: string
expectedStatus?: HttpStatusCode
}) {
- const { server, password = server.store.user.password, otpToken, expectedStatus } = options
+ const { server, username, password, otpToken, expectedStatus } = options
- const user = { username: server.store.user.username, password }
+ const user = { username, password }
const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus })
return { res, token }
@@ -21,23 +22,28 @@ async function login (options: {
describe('Test users', function () {
let server: PeerTubeServer
- let rootId: number
let otpSecret: string
let requestToken: string
+ const userUsername = 'user1'
+ let userId: number
+ let userPassword: string
+ let userToken: string
+
before(async function () {
this.timeout(30000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
-
- const { id } = await server.users.getMyInfo()
- rootId = id
+ const res = await server.users.generate(userUsername)
+ userId = res.userId
+ userPassword = res.password
+ userToken = res.token
})
it('Should not add the header on login if two factor is not enabled', async function () {
- const { res, token } = await login({ server })
+ const { res, token } = await login({ server, username: userUsername, password: userPassword })
expect(res.header['x-peertube-otp']).to.not.exist
@@ -45,10 +51,7 @@ describe('Test users', function () {
})
it('Should request two factor and get the secret and uri', async function () {
- const { otpRequest } = await server.twoFactor.request({
- userId: rootId,
- currentPassword: server.store.user.password
- })
+ const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
expect(otpRequest.requestToken).to.exist
@@ -64,27 +67,33 @@ describe('Test users', function () {
})
it('Should not have two factor confirmed yet', async function () {
- const { twoFactorEnabled } = await server.users.getMyInfo()
+ const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
expect(twoFactorEnabled).to.be.false
})
it('Should confirm two factor', async function () {
await server.twoFactor.confirmRequest({
- userId: rootId,
+ userId,
+ token: userToken,
otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(),
requestToken
})
})
it('Should not add the header on login if two factor is enabled and password is incorrect', async function () {
- const { res, token } = await login({ server, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ const { res, token } = await login({ server, username: userUsername, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
expect(res.header['x-peertube-otp']).to.not.exist
expect(token).to.not.exist
})
it('Should add the header on login if two factor is enabled and password is correct', async function () {
- const { res, token } = await login({ server, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+ const { res, token } = await login({
+ server,
+ username: userUsername,
+ password: userPassword,
+ expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+ })
expect(res.header['x-peertube-otp']).to.exist
expect(token).to.not.exist
@@ -95,14 +104,26 @@ describe('Test users', function () {
it('Should not login with correct password and incorrect otp secret', async function () {
const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) })
- const { res, token } = await login({ server, otpToken: otp.generate(), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ const { res, token } = await login({
+ server,
+ username: userUsername,
+ password: userPassword,
+ otpToken: otp.generate(),
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
expect(res.header['x-peertube-otp']).to.not.exist
expect(token).to.not.exist
})
it('Should not login with correct password and incorrect otp code', async function () {
- const { res, token } = await login({ server, otpToken: '123456', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ const { res, token } = await login({
+ server,
+ username: userUsername,
+ password: userPassword,
+ otpToken: '123456',
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
expect(res.header['x-peertube-otp']).to.not.exist
expect(token).to.not.exist
@@ -111,7 +132,13 @@ describe('Test users', function () {
it('Should not login with incorrect password and correct otp code', async function () {
const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
- const { res, token } = await login({ server, password: 'fake', otpToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ const { res, token } = await login({
+ server,
+ username: userUsername,
+ password: 'fake',
+ otpToken,
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
expect(res.header['x-peertube-otp']).to.not.exist
expect(token).to.not.exist
@@ -120,7 +147,7 @@ describe('Test users', function () {
it('Should correctly login with correct password and otp code', async function () {
const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
- const { res, token } = await login({ server, otpToken })
+ const { res, token } = await login({ server, username: userUsername, password: userPassword, otpToken })
expect(res.header['x-peertube-otp']).to.not.exist
expect(token).to.exist
@@ -129,21 +156,41 @@ describe('Test users', function () {
})
it('Should have two factor enabled when getting my info', async function () {
- const { twoFactorEnabled } = await server.users.getMyInfo()
+ const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
expect(twoFactorEnabled).to.be.true
})
it('Should disable two factor and be able to login without otp token', async function () {
- await server.twoFactor.disable({ userId: rootId, currentPassword: server.store.user.password })
+ await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword })
- const { res, token } = await login({ server })
+ const { res, token } = await login({ server, username: userUsername, password: userPassword })
expect(res.header['x-peertube-otp']).to.not.exist
await server.users.getMyInfo({ token })
})
it('Should have two factor disabled when getting my info', async function () {
- const { twoFactorEnabled } = await server.users.getMyInfo()
+ const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
+ expect(twoFactorEnabled).to.be.false
+ })
+
+ it('Should enable two factor auth without password from an admin', async function () {
+ const { otpRequest } = await server.twoFactor.request({ userId })
+
+ await server.twoFactor.confirmRequest({
+ userId,
+ otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate(),
+ requestToken: otpRequest.requestToken
+ })
+
+ const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
+ expect(twoFactorEnabled).to.be.true
+ })
+
+ it('Should disable two factor auth without password from an admin', async function () {
+ await server.twoFactor.disable({ userId })
+
+ const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
expect(twoFactorEnabled).to.be.false
})
diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts
index 6c9d270ae..5542acfda 100644
--- a/shared/server-commands/users/two-factor-command.ts
+++ b/shared/server-commands/users/two-factor-command.ts
@@ -21,7 +21,7 @@ export class TwoFactorCommand extends AbstractCommand {
request (options: OverrideCommandOptions & {
userId: number
- currentPassword: string
+ currentPassword?: string
}) {
const { currentPassword, userId } = options
@@ -58,7 +58,7 @@ export class TwoFactorCommand extends AbstractCommand {
disable (options: OverrideCommandOptions & {
userId: number
- currentPassword: string
+ currentPassword?: string
}) {
const { userId, currentPassword } = options
const path = '/api/v1/users/' + userId + '/two-factor/disable'
@@ -72,4 +72,21 @@ export class TwoFactorCommand extends AbstractCommand {
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
+
+ async requestAndConfirm (options: OverrideCommandOptions & {
+ userId: number
+ currentPassword?: string
+ }) {
+ const { userId, currentPassword } = options
+
+ const { otpRequest } = await this.request({ userId, currentPassword })
+
+ await this.confirmRequest({
+ userId,
+ requestToken: otpRequest.requestToken,
+ otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
+ })
+
+ return otpRequest
+ }
}