Merge branch 'feature/otp' into develop

This commit is contained in:
Chocobozzz 2022-10-10 11:19:58 +02:00
commit 63fa260a81
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
128 changed files with 2164 additions and 385 deletions

View File

@ -7,7 +7,7 @@ import {
FROM_NAME_VALIDATOR,
SUBJECT_VALIDATOR
} from '@app/shared/form-validators/instance-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { InstanceService } from '@app/shared/shared-instance'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -32,7 +32,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
private serverConfig: HTMLServerConfig
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private router: Router,
private modalService: NgbModal,
private instanceService: InstanceService,

View File

@ -18,15 +18,15 @@ import {
MAX_INSTANCE_LIVES_VALIDATOR,
MAX_LIVE_DURATION_VALIDATOR,
MAX_USER_LIVES_VALIDATOR,
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
SEARCH_INDEX_URL_VALIDATOR,
SERVICES_TWITTER_USERNAME_VALIDATOR,
SIGNUP_LIMIT_VALIDATOR,
SIGNUP_MINIMUM_AGE_VALIDATOR,
TRANSCODING_THREADS_VALIDATOR,
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
TRANSCODING_THREADS_VALIDATOR
} from '@app/shared/form-validators/custom-config-validators'
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { CustomPageService } from '@app/shared/shared-main/custom-page'
import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models'
import { EditConfigurationService } from './edit-configuration.service'
@ -52,9 +52,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
categoryItems: SelectOptionsItem[] = []
constructor (
protected formReactiveService: FormReactiveService,
private router: Router,
private route: ActivatedRoute,
protected formValidatorService: FormValidatorService,
private notifier: Notifier,
private configService: ConfigService,
private customPage: CustomPageService,

View File

@ -2,7 +2,7 @@ import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/cor
import { Notifier } from '@app/core'
import { prepareIcu } from '@app/helpers'
import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { InstanceFollowService } from '@app/shared/shared-instance'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -22,7 +22,7 @@ export class FollowModalComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private followService: InstanceFollowService,
private notifier: Notifier

View File

@ -12,7 +12,7 @@ import {
USER_VIDEO_QUOTA_DAILY_VALIDATOR,
USER_VIDEO_QUOTA_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { UserAdminService } from '@app/shared/shared-users'
import { UserCreate, UserRole } from '@shared/models'
import { UserEdit } from './user-edit'
@ -27,7 +27,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
constructor (
protected serverService: ServerService,
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected configService: ConfigService,
protected screenService: ScreenService,
protected auth: AuthService,

View File

@ -204,7 +204,7 @@
</div>
<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid -->
<div *ngIf="displayDangerZone()" class="row mt-4"> <!-- danger zone grid -->
<div class="col-12 col-lg-4 col-xl-3">
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
<div i18n class="account-title account-title-danger">DANGER ZONE</div>
@ -213,7 +213,7 @@
<div class="col-12 col-lg-8 col-xl-9">
<div class="danger-zone">
<div class="form-group reset-password-email">
<div class="form-group">
<label i18n>Send a link to reset the password by email to the user</label>
<button (click)="resetPassword()" i18n>Ask for new password</button>
</div>
@ -222,6 +222,11 @@
<label i18n>Manually set the user password</label>
<my-user-password [userId]="user.id"></my-user-password>
</div>
<div *ngIf="user.twoFactorEnabled" class="form-group">
<label i18n>This user has two factor authentication enabled</label>
<button (click)="disableTwoFactorAuth()" i18n>Disable two factor authentication</button>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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']
}

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core'
import { Notifier } from '@app/core'
import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { UserAdminService } from '@app/shared/shared-users'
import { UserUpdate } from '@shared/models'
@ -18,7 +18,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
@Input() userId: number
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private notifier: Notifier,
private userAdminService: UserAdminService
) {

View File

@ -9,8 +9,8 @@ import {
USER_VIDEO_QUOTA_DAILY_VALIDATOR,
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 { FormReactiveService } from '@app/shared/shared-forms'
import { TwoFactorService, UserAdminService } from '@app/shared/shared-users'
import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
import { UserEdit } from './user-edit'
@ -25,7 +25,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
private paramsSub: Subscription
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected serverService: ServerService,
protected configService: ConfigService,
protected screenService: ScreenService,
@ -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)

View File

@ -1,6 +1,6 @@
@use '_variables' as *;
@use '_mixins' as *;
@use '~bootstrap/scss/functions' as *;
@use 'bootstrap/scss/functions' as *;
.add-button {
@include create-button;

View File

@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { HooksService, Notifier, PluginService } from '@app/core'
import { BuildFormArgument } from '@app/shared/form-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models'
import { PluginApiService } from '../shared/plugin-api.service'
@ -22,7 +22,7 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
private npmName: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private pluginService: PluginService,
private pluginAPIService: PluginApiService,
private notifier: Notifier,

View File

@ -39,34 +39,48 @@
<div class="login-form-and-externals">
<form myPluginSelector pluginSelectorId="login-form" role="form" (ngSubmit)="login()" [formGroup]="form">
<div class="form-group">
<div>
<label i18n for="username">Username or email address</label>
<input
type="text" id="username" i18n-placeholder placeholder="Example: john@example.com" required tabindex="1"
formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus
>
<ng-container *ngIf="!otpStep">
<div class="form-group">
<div>
<label i18n for="username">Username or email address</label>
<input
type="text" id="username" i18n-placeholder placeholder="Example: john@example.com" required tabindex="1"
formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus
>
</div>
<div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
<div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
⚠️ Most email addresses do not include capital letters.
</div>
</div>
<div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
<div class="form-group">
<label i18n for="password">Password</label>
<div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
⚠️ Most email addresses do not include capital letters.
<my-input-text
formControlName="password" inputId="password" i18n-placeholder placeholder="Password"
[formError]="formErrors['password']" autocomplete="current-password" [tabindex]="2"
></my-input-text>
</div>
</div>
</ng-container>
<div class="form-group">
<label i18n for="password">Password</label>
<div *ngIf="otpStep" class="form-group">
<p i18n>Enter the two-factor code generated by your phone app:</p>
<label i18n for="otp-token">Two factor authentication token</label>
<my-input-text
formControlName="password" inputId="password" i18n-placeholder placeholder="Password"
[formError]="formErrors['password']" autocomplete="current-password" [tabindex]="2"
#otpTokenInput
[show]="true" formControlName="otp-token" inputId="otp-token"
[formError]="formErrors['otp-token']" autocomplete="otp-token"
></my-input-text>
</div>
<input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
<div class="additional-links">
<div *ngIf="!otpStep" class="additional-links">
<a i18n role="button" class="link-orange" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
<ng-container *ngIf="signupAllowed">

View File

@ -1,8 +1,8 @@
@use '_variables' as *;
@use '_mixins' as *;
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
label {
display: block;

View File

@ -1,10 +1,10 @@
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms'
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { PluginsManager } from '@root-helpers/plugins-manager'
@ -20,6 +20,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
private static SESSION_STORAGE_REDIRECT_URL_KEY = 'login-previous-url'
@ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
@ViewChild('otpTokenInput') otpTokenInput: InputTextComponent
accordion: NgbAccordion
error: string = null
@ -37,11 +38,13 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
codeOfConduct: false
}
otpStep = false
private openedForgotPasswordModal: NgbModalRef
private serverConfig: ServerConfig
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private route: ActivatedRoute,
private modalService: NgbModal,
private authService: AuthService,
@ -82,7 +85,11 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
// Avoid undefined errors when accessing form error properties
this.buildForm({
username: LOGIN_USERNAME_VALIDATOR,
password: LOGIN_PASSWORD_VALIDATOR
password: LOGIN_PASSWORD_VALIDATOR,
'otp-token': {
VALIDATORS: [], // Will be set dynamically
MESSAGES: USER_OTP_TOKEN_VALIDATOR.MESSAGES
}
})
this.serverConfig = snapshot.data.serverConfig
@ -118,13 +125,20 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
login () {
this.error = null
const { username, password } = this.form.value
const options = {
username: this.form.value['username'],
password: this.form.value['password'],
otpToken: this.form.value['otp-token']
}
this.authService.login(username, password)
this.authService.login(options)
.pipe()
.subscribe({
next: () => this.redirectService.redirectToPreviousRoute(),
error: err => this.handleError(err)
error: err => {
this.handleError(err)
}
})
}
@ -162,7 +176,7 @@ The link will expire within 1 hour.`
private loadExternalAuthToken (username: string, token: string) {
this.isAuthenticatedWithExternalAuth = true
this.authService.login(username, null, token)
this.authService.login({ username, password: null, token })
.subscribe({
next: () => {
const redirectUrl = this.storage.getItem(LoginComponent.SESSION_STORAGE_REDIRECT_URL_KEY)
@ -182,6 +196,17 @@ The link will expire within 1 hour.`
}
private handleError (err: any) {
if (this.authService.isOTPMissingError(err)) {
this.otpStep = true
setTimeout(() => {
this.form.get('otp-token').setValidators(USER_OTP_TOKEN_VALIDATOR.VALIDATORS)
this.otpTokenInput.focus()
})
return
}
if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.`
else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.`
else this.error = err.message

View File

@ -9,7 +9,7 @@ import {
VIDEO_CHANNEL_NAME_VALIDATOR,
VIDEO_CHANNEL_SUPPORT_VALIDATOR
} from '@app/shared/form-validators/video-channel-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
import { VideoChannelEdit } from './video-channel-edit'
@ -26,7 +26,7 @@ export class VideoChannelCreateComponent extends VideoChannelEdit implements OnI
private banner: FormData
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private router: Router,

View File

@ -9,7 +9,7 @@ import {
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
VIDEO_CHANNEL_SUPPORT_VALIDATOR
} from '@app/shared/form-validators/video-channel-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
import { VideoChannelEdit } from './video-channel-edit'
@ -28,7 +28,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
private serverConfig: HTMLServerConfig
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private route: ActivatedRoute,

View File

@ -7,6 +7,7 @@ import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-b
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
import { MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
import { MyAccountComponent } from './my-account.component'
const myAccountRoutes: Routes = [
@ -30,6 +31,16 @@ const myAccountRoutes: Routes = [
}
},
{
path: 'two-factor-auth',
component: MyAccountTwoFactorComponent,
data: {
meta: {
title: $localize`Two factor authentication`
}
}
},
{
path: 'video-channels',
redirectTo: '/my-library/video-channels',

View File

@ -3,8 +3,8 @@ import { tap } from 'rxjs/operators'
import { Component, OnInit } from '@angular/core'
import { AuthService, ServerService, UserService } from '@app/core'
import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { User } from '@shared/models'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { HttpStatusCode, User } from '@shared/models'
@Component({
selector: 'my-account-change-email',
@ -17,7 +17,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
user: User = null
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private userService: UserService,
private serverService: ServerService
@ -57,7 +57,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
},
error: err => {
if (err.status === 401) {
if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
this.error = $localize`You current password is invalid.`
return
}

View File

@ -6,8 +6,8 @@ import {
USER_EXISTING_PASSWORD_VALIDATOR,
USER_PASSWORD_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { User } from '@shared/models'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { HttpStatusCode, User } from '@shared/models'
@Component({
selector: 'my-account-change-password',
@ -19,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
user: User = null
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private notifier: Notifier,
private authService: AuthService,
private userService: UserService
@ -57,7 +57,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
},
error: err => {
if (err.status === 401) {
if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
this.error = $localize`You current password is invalid.`
return
}

View File

@ -18,7 +18,7 @@ export class MyAccountDangerZoneComponent {
) { }
async deleteMe () {
const res = await this.confirmService.confirmWithInput(
const res = await this.confirmService.confirmWithExpectedInput(
$localize`Are you sure you want to delete your account?` +
'<br /><br />' +
// eslint-disable-next-line max-len

View File

@ -2,7 +2,7 @@ import { Subject } from 'rxjs'
import { Component, Input, OnInit } from '@angular/core'
import { Notifier, User, UserService } from '@app/core'
import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
@Component({
selector: 'my-account-profile',
@ -16,7 +16,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
error: string = null
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private notifier: Notifier,
private userService: UserService
) {

View File

@ -62,6 +62,16 @@
</div>
</div>
<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- two factor auth grid -->
<div class="col-12 col-lg-4 col-xl-3">
<h2 i18n class="account-title">Two-factor authentication</h2>
</div>
<div class="col-12 col-lg-8 col-xl-9">
<my-account-two-factor-button [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-two-factor-button>
</div>
</div>
<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- email grid -->
<div class="col-12 col-lg-4 col-xl-3">
<h2 i18n class="account-title">EMAIL</h2>

View File

@ -1,6 +1,6 @@
@use '_variables' as *;
@use '_mixins' as *;
@use '~bootstrap/scss/functions' as *;
@use 'bootstrap/scss/functions' as *;
.account-title {
@include settings-big-title;

View File

@ -0,0 +1,2 @@
export * from './my-account-two-factor-button.component'
export * from './my-account-two-factor.component'

View File

@ -0,0 +1,12 @@
<div class="two-factor">
<ng-container *ngIf="!twoFactorEnabled">
<p i18n>Two factor authentication adds an additional layer of security to your account by requiring a numeric code from another device (most commonly mobile phones) when you log in.</p>
<my-button [routerLink]="[ '/my-account/two-factor-auth' ]" className="orange-button-link" i18n>Enable two-factor authentication</my-button>
</ng-container>
<ng-container *ngIf="twoFactorEnabled">
<my-button className="orange-button" (click)="disableTwoFactor()" i18n>Disable two-factor authentication</my-button>
</ng-container>
</div>

View File

@ -0,0 +1,49 @@
import { Subject } from 'rxjs'
import { Component, Input, OnInit } from '@angular/core'
import { AuthService, ConfirmService, Notifier, User } from '@app/core'
import { TwoFactorService } from '@app/shared/shared-users'
@Component({
selector: 'my-account-two-factor-button',
templateUrl: './my-account-two-factor-button.component.html'
})
export class MyAccountTwoFactorButtonComponent implements OnInit {
@Input() user: User = null
@Input() userInformationLoaded: Subject<any>
twoFactorEnabled = false
constructor (
private notifier: Notifier,
private twoFactorService: TwoFactorService,
private confirmService: ConfirmService,
private auth: AuthService
) {
}
ngOnInit () {
this.userInformationLoaded.subscribe(() => {
this.twoFactorEnabled = this.user.twoFactorEnabled
})
}
async disableTwoFactor () {
const message = $localize`Are you sure you want to disable two factor authentication of your account?`
const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`)
if (confirmed === false) return
this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password })
.subscribe({
next: () => {
this.twoFactorEnabled = false
this.auth.refreshUserInformation()
this.notifier.success($localize`Two factor authentication disabled`)
},
error: err => this.notifier.error(err.message)
})
}
}

View File

@ -0,0 +1,54 @@
<h1>
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<ng-container i18n>Two factor authentication</ng-container>
</h1>
<div i18n *ngIf="twoFactorAlreadyEnabled === true" class="root already-enabled">
Two factor authentication is already enabled.
</div>
<div class="root" *ngIf="twoFactorAlreadyEnabled === false">
<ng-container *ngIf="step === 'request'">
<form role="form" (ngSubmit)="requestTwoFactor()" [formGroup]="formPassword">
<label i18n for="current-password">Your password</label>
<div class="form-group-description" i18n>Confirm your password to enable two factor authentication</div>
<my-input-text
formControlName="current-password" inputId="current-password" i18n-placeholder placeholder="Current password"
[formError]="formErrorsPassword['current-password']" autocomplete="current-password"
></my-input-text>
<input class="peertube-button orange-button mt-3" type="submit" i18n-value value="Confirm" [disabled]="!formPassword.valid">
</form>
</ng-container>
<ng-container *ngIf="step === 'confirm'">
<p i18n>
Scan this QR code into a TOTP app on your phone. This app will generate tokens that you will have to enter when logging in.
</p>
<qrcode [qrdata]="twoFactorURI" [width]="256" level="Q"></qrcode>
<div i18n>
If you can't scan the QR code and need to enter it manually, here is the plain-text secret:
</div>
<div class="secret-plain-text">{{ twoFactorSecret }}</div>
<form class="mt-3" role="form" (ngSubmit)="confirmTwoFactor()" [formGroup]="formOTP">
<label i18n for="otp-token">Two-factor code</label>
<div class="form-group-description" i18n>Enter the code generated by your authenticator app to confirm</div>
<my-input-text
[show]="true" formControlName="otp-token" inputId="otp-token"
[formError]="formErrorsOTP['otp-token']" autocomplete="otp-token"
></my-input-text>
<input class="peertube-button orange-button mt-3" type="submit" i18n-value value="Confirm" [disabled]="!formOTP.valid">
</form>
</ng-container>
</div>

View File

@ -0,0 +1,16 @@
@use '_variables' as *;
@use '_mixins' as *;
.root {
max-width: 600px;
}
.secret-plain-text {
font-family: monospace;
font-size: 0.9rem;
}
qrcode {
display: inline-block;
margin: auto;
}

View File

@ -0,0 +1,105 @@
import { Component, OnInit } from '@angular/core'
import { FormGroup } from '@angular/forms'
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 '@app/shared/shared-users'
@Component({
selector: 'my-account-two-factor',
templateUrl: './my-account-two-factor.component.html',
styleUrls: [ './my-account-two-factor.component.scss' ]
})
export class MyAccountTwoFactorComponent implements OnInit {
twoFactorAlreadyEnabled: boolean
step: 'request' | 'confirm' | 'confirmed' = 'request'
twoFactorSecret: string
twoFactorURI: string
inPasswordStep = true
formPassword: FormGroup
formErrorsPassword: any
formOTP: FormGroup
formErrorsOTP: any
private user: User
private requestToken: string
constructor (
private notifier: Notifier,
private twoFactorService: TwoFactorService,
private formReactiveService: FormReactiveService,
private auth: AuthService,
private router: Router
) {
}
ngOnInit () {
this.buildPasswordForm()
this.buildOTPForm()
this.auth.userInformationLoaded.subscribe(() => {
this.user = this.auth.getUser()
this.twoFactorAlreadyEnabled = this.user.twoFactorEnabled
})
}
requestTwoFactor () {
this.twoFactorService.requestTwoFactor({
userId: this.user.id,
currentPassword: this.formPassword.value['current-password']
}).subscribe({
next: ({ otpRequest }) => {
this.requestToken = otpRequest.requestToken
this.twoFactorURI = otpRequest.uri
this.twoFactorSecret = otpRequest.secret.replace(/(.{4})/g, '$1 ').trim()
this.step = 'confirm'
},
error: err => this.notifier.error(err.message)
})
}
confirmTwoFactor () {
this.twoFactorService.confirmTwoFactorRequest({
userId: this.user.id,
requestToken: this.requestToken,
otpToken: this.formOTP.value['otp-token']
}).subscribe({
next: () => {
this.notifier.success($localize`Two factor authentication has been enabled.`)
this.auth.refreshUserInformation()
this.router.navigateByUrl('/my-account/settings')
},
error: err => this.notifier.error(err.message)
})
}
private buildPasswordForm () {
const { form, formErrors } = this.formReactiveService.buildForm({
'current-password': USER_EXISTING_PASSWORD_VALIDATOR
})
this.formPassword = form
this.formErrorsPassword = formErrors
}
private buildOTPForm () {
const { form, formErrors } = this.formReactiveService.buildForm({
'otp-token': USER_OTP_TOKEN_VALIDATOR
})
this.formOTP = form
this.formErrorsOTP = formErrors
}
}

View File

@ -1,3 +1,4 @@
import { QRCodeModule } from 'angularx-qrcode'
import { AutoCompleteModule } from 'primeng/autocomplete'
import { TableModule } from 'primeng/table'
import { DragDropModule } from '@angular/cdk/drag-drop'
@ -10,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'
@ -23,12 +25,14 @@ 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 } from './my-account-settings/my-account-two-factor'
import { MyAccountComponent } from './my-account.component'
@NgModule({
imports: [
MyAccountRoutingModule,
QRCodeModule,
AutoCompleteModule,
TableModule,
DragDropModule,
@ -37,6 +41,7 @@ import { MyAccountComponent } from './my-account.component'
SharedFormModule,
SharedModerationModule,
SharedUserInterfaceSettingsModule,
SharedUsersModule,
SharedGlobalIconModule,
SharedAbuseListModule,
SharedShareModal,
@ -52,6 +57,9 @@ import { MyAccountComponent } from './my-account.component'
MyAccountChangeEmailComponent,
MyAccountApplicationsComponent,
MyAccountTwoFactorButtonComponent,
MyAccountTwoFactorComponent,
MyAccountDangerZoneComponent,
MyAccountBlocklistComponent,
MyAccountAbusesListComponent,

View File

@ -40,7 +40,7 @@ export class MyVideoChannelsComponent {
}
async deleteVideoChannel (videoChannel: VideoChannel) {
const res = await this.confirmService.confirmWithInput(
const res = await this.confirmService.confirmWithExpectedInput(
$localize`Do you really want to delete ${videoChannel.displayName}?
It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
channel with the same name (${videoChannel.name})!`,

View File

@ -3,7 +3,7 @@ import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '
import { AuthService, Notifier } from '@app/core'
import { listUserChannelsForSelect } from '@app/helpers'
import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoOwnershipService } from '@app/shared/shared-main'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { VideoChangeOwnership } from '@shared/models'
@ -24,7 +24,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
error: string = null
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private videoOwnershipService: VideoOwnershipService,
private notifier: Notifier,
private authService: AuthService,

View File

@ -5,7 +5,7 @@ import { Router } from '@angular/router'
import { AuthService, Notifier } from '@app/core'
import { listUserChannelsForSelect } from '@app/helpers'
import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main'
import { VideoChannelSyncCreate } from '@shared/models/videos'
@ -20,7 +20,7 @@ export class VideoChannelSyncEditComponent extends FormReactive implements OnIni
existingVideosStrategy: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private router: Router,
private notifier: Notifier,

View File

@ -9,7 +9,7 @@ import {
VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
VIDEO_PLAYLIST_PRIVACY_VALIDATOR
} from '@app/shared/form-validators/video-playlist-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
@ -23,7 +23,7 @@ export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implemen
error: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private router: Router,

View File

@ -11,7 +11,7 @@ import {
VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
VIDEO_PLAYLIST_PRIVACY_VALIDATOR
} from '@app/shared/form-validators/video-playlist-validators'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { VideoPlaylistUpdate } from '@shared/models'
import { MyVideoPlaylistEdit } from './my-video-playlist-edit'
@ -27,7 +27,7 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen
private paramsSub: Subscription
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private router: Router,

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { Notifier, UserService } from '@app/core'
import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { Video, VideoOwnershipService } from '@app/shared/shared-main'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@ -20,7 +20,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
private video: Video | undefined = undefined
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private videoOwnershipService: VideoOwnershipService,
private notifier: Notifier,
private userService: UserService,

View File

@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'
import { Notifier, UserService } from '@app/core'
import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators'
import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
@Component({
selector: 'my-login',
@ -16,7 +16,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
private verificationString: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private userService: UserService,
private notifier: Notifier,
private router: Router,

View File

@ -158,7 +158,7 @@ export class RegisterComponent implements OnInit {
}
// Auto login
this.authService.login(body.username, body.password)
this.authService.login({ username: body.username, password: body.password })
.subscribe({
next: () => {
this.signupSuccess = true

View File

@ -3,7 +3,7 @@ import { pairwise } from 'rxjs/operators'
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { UserSignupService } from '@app/shared/shared-users'
@Component({
@ -19,7 +19,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
@Output() formBuilt = new EventEmitter<FormGroup>()
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private userSignupService: UserSignupService
) {
super()

View File

@ -1,9 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { FormGroup } from '@angular/forms'
import {
USER_TERMS_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
@Component({
selector: 'my-register-step-terms',
@ -19,7 +17,7 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit {
@Output() codeOfConductClick = new EventEmitter<void>()
constructor (
protected formValidatorService: FormValidatorService
protected formReactiveService: FormReactiveService
) {
super()
}

View File

@ -8,7 +8,7 @@ import {
USER_PASSWORD_VALIDATOR,
USER_USERNAME_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { UserSignupService } from '@app/shared/shared-users'
@Component({
@ -23,7 +23,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
@Output() formBuilt = new EventEmitter<FormGroup>()
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private userSignupService: UserSignupService
) {
super()

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'
import { Notifier, RedirectService, ServerService } from '@app/core'
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { UserSignupService } from '@app/shared/shared-users'
@Component({
@ -14,7 +14,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
requiresEmailVerification = false
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private userSignupService: UserSignupService,
private serverService: ServerService,
private notifier: Notifier,

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ConfirmService, Notifier, ServerService } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoDetails } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { logger } from '@root-helpers/logger'
@ -20,7 +20,7 @@ export class VideoStudioEditComponent extends FormReactive implements OnInit {
video: VideoDetails
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private serverService: ServerService,
private notifier: Notifier,
private router: Router,

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { ServerService } from '@app/core'
import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoCaptionEdit } from '@app/shared/shared-main'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { HTMLServerConfig, VideoConstant } from '@shared/models'
@ -26,7 +26,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
private closingModal = false
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private serverService: ServerService
) {

View File

@ -1,8 +1,8 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { HTMLServerConfig, VideoConstant } from '@shared/models'
import { ServerService } from '../../../../core'
@ -29,8 +29,7 @@ export class VideoCaptionEditModalContentComponent extends FormReactive implemen
constructor (
protected openedModal: NgbActiveModal,
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
protected formReactiveService: FormReactiveService,
private videoCaptionService: VideoCaptionService,
private serverService: ServerService
) {

View File

@ -3,7 +3,7 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular
import { Router } from '@angular/router'
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
import { scrollToTop } from '@app/helpers'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core'
@ -39,7 +39,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
error: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected loadingBar: LoadingBarService,
protected notifier: Notifier,
protected authService: AuthService,

View File

@ -3,7 +3,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, Vie
import { Router } from '@angular/router'
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
import { scrollToTop } from '@app/helpers'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { logger } from '@root-helpers/logger'
@ -35,7 +35,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
error: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected loadingBar: LoadingBarService,
protected notifier: Notifier,
protected authService: AuthService,

View File

@ -4,7 +4,7 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular
import { Router } from '@angular/router'
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
import { scrollToTop } from '@app/helpers'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { logger } from '@root-helpers/logger'
@ -34,7 +34,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
error: string
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected loadingBar: LoadingBarService,
protected notifier: Notifier,
protected authService: AuthService,

View File

@ -5,7 +5,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit,
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
import { FormValidatorService } from '@app/shared/shared-forms'
import { FormReactiveService } from '@app/shared/shared-forms'
import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { logger } from '@root-helpers/logger'
@ -60,7 +60,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
private uploadServiceSubscription: Subscription
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
protected loadingBar: LoadingBarService,
protected notifier: Notifier,
protected authService: AuthService,

View File

@ -4,7 +4,7 @@ import { SelectChannelItem } from 'src/types/select-options-item.model'
import { Component, HostListener, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core'
@ -33,7 +33,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
private updateDone = false
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private route: ActivatedRoute,
private router: Router,
private notifier: Notifier,

View File

@ -16,7 +16,7 @@ import {
import { Router } from '@angular/router'
import { Notifier, User } from '@app/core'
import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { Video } from '@app/shared/shared-main'
import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@ -48,7 +48,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
private emojiMarkupList: { emoji: string, name: string }[]
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private notifier: Notifier,
private videoCommentService: VideoCommentService,
private modalService: NgbModal,

View File

@ -1,7 +1,7 @@
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs'
import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import { Notifier } from '@app/core/notification/notifier.service'
@ -141,7 +141,14 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
return !!this.getAccessToken()
}
login (username: string, password: string, token?: string) {
login (options: {
username: string
password: string
otpToken?: string
token?: string
}) {
const { username, password, token, otpToken } = options
// Form url encoded
const body = {
client_id: this.clientId,
@ -155,7 +162,9 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
if (token) Object.assign(body, { externalAuthToken: token })
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
let headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
if (otpToken) headers = headers.set('x-peertube-otp', otpToken)
return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers })
.pipe(
map(res => Object.assign(res, { username })),
@ -245,6 +254,14 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
})
}
isOTPMissingError (err: HttpErrorResponse) {
if (err.status !== HttpStatusCode.UNAUTHORIZED_401) return false
if (err.headers.get('x-peertube-otp') !== 'required; app') return false
return true
}
private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
// User is not loaded yet, set manually auth header
const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)

View File

@ -1,28 +1,53 @@
import { firstValueFrom, Subject } from 'rxjs'
import { firstValueFrom, map, Observable, Subject } from 'rxjs'
import { Injectable } from '@angular/core'
type ConfirmOptions = {
title: string
message: string
inputLabel?: string
expectedInputValue?: string
confirmButtonText?: string
}
} & (
{
type: 'confirm'
confirmButtonText?: string
} |
{
type: 'confirm-password'
confirmButtonText?: string
} |
{
type: 'confirm-expected-input'
inputLabel?: string
expectedInputValue?: string
confirmButtonText?: string
}
)
@Injectable()
export class ConfirmService {
showConfirm = new Subject<ConfirmOptions>()
confirmResponse = new Subject<boolean>()
confirmResponse = new Subject<{ confirmed: boolean, value?: string }>()
confirm (message: string, title = '', confirmButtonText?: string) {
this.showConfirm.next({ title, message, confirmButtonText })
this.showConfirm.next({ type: 'confirm', title, message, confirmButtonText })
return firstValueFrom(this.confirmResponse.asObservable())
return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable()))
}
confirmWithInput (message: string, inputLabel: string, expectedInputValue: string, title = '', confirmButtonText?: string) {
this.showConfirm.next({ title, message, inputLabel, expectedInputValue, confirmButtonText })
confirmWithPassword (message: string, title = '', confirmButtonText?: string) {
this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText })
return firstValueFrom(this.confirmResponse.asObservable())
const obs = this.confirmResponse.asObservable()
.pipe(map(({ confirmed, value }) => ({ confirmed, password: value })))
return firstValueFrom(obs)
}
confirmWithExpectedInput (message: string, inputLabel: string, expectedInputValue: string, title = '', confirmButtonText?: string) {
this.showConfirm.next({ type: 'confirm-expected-input', title, message, inputLabel, expectedInputValue, confirmButtonText })
return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable()))
}
private extractConfirmed (obs: Observable<{ confirmed: boolean }>) {
return obs.pipe(map(({ confirmed }) => confirmed))
}
}

View File

@ -4,6 +4,7 @@ import { Router } from '@angular/router'
import { DateFormat, dateToHuman } from '@app/helpers'
import { logger } from '@root-helpers/logger'
import { HttpStatusCode, ResultList } from '@shared/models'
import { HttpHeaderResponse } from '@angular/common/http'
@Injectable()
export class RestExtractor {
@ -54,10 +55,11 @@ export class RestExtractor {
handleError (err: any) {
const errorMessage = this.buildErrorMessage(err)
const errorObj: { message: string, status: string, body: string } = {
const errorObj: { message: string, status: string, body: string, headers: HttpHeaderResponse } = {
message: errorMessage,
status: undefined,
body: undefined
body: undefined,
headers: err.headers
}
if (err.status) {

View File

@ -66,6 +66,8 @@ export class User implements UserServerModel {
lastLoginDate: Date | null
twoFactorEnabled: boolean
createdAt: Date
constructor (hash: Partial<UserServerModel>) {
@ -108,6 +110,8 @@ export class User implements UserServerModel {
this.notificationSettings = hash.notificationSettings
this.twoFactorEnabled = hash.twoFactorEnabled
this.createdAt = hash.createdAt
this.pluginAuth = hash.pluginAuth

View File

@ -9,9 +9,12 @@
<div class="modal-body" >
<div [innerHtml]="message"></div>
<div *ngIf="inputLabel && expectedInputValue" class="form-group mt-3">
<div *ngIf="inputLabel" class="form-group mt-3">
<label for="confirmInput">{{ inputLabel }}</label>
<input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
<input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
<my-input-text inputId="confirmInput" [(ngModel)]="inputValue"></my-input-text>
</div>
</div>

View File

@ -21,6 +21,8 @@ export class ConfirmComponent implements OnInit {
inputValue = ''
confirmButtonText = ''
isPasswordInput = false
private openedModal: NgbModalRef
constructor (
@ -31,11 +33,27 @@ export class ConfirmComponent implements OnInit {
ngOnInit () {
this.confirmService.showConfirm.subscribe(
({ title, message, expectedInputValue, inputLabel, confirmButtonText }) => {
payload => {
// Reinit fields
this.title = ''
this.message = ''
this.expectedInputValue = ''
this.inputLabel = ''
this.inputValue = ''
this.confirmButtonText = ''
this.isPasswordInput = false
const { type, title, message, confirmButtonText } = payload
this.title = title
this.inputLabel = inputLabel
this.expectedInputValue = expectedInputValue
if (type === 'confirm-expected-input') {
this.inputLabel = payload.inputLabel
this.expectedInputValue = payload.expectedInputValue
} else if (type === 'confirm-password') {
this.inputLabel = $localize`Confirm your password`
this.isPasswordInput = true
}
this.confirmButtonText = confirmButtonText || $localize`Confirm`
@ -66,11 +84,13 @@ export class ConfirmComponent implements OnInit {
this.openedModal = this.modalService.open(this.confirmModal, { centered: true })
this.openedModal.result
.then(() => this.confirmService.confirmResponse.next(true))
.then(() => {
this.confirmService.confirmResponse.next({ confirmed: true, value: this.inputValue })
})
.catch((reason: string) => {
// If the reason was that the user used the back button, we don't care about the confirm dialog result
if (!reason || reason !== POP_STATE_MODAL_DISMISS) {
this.confirmService.confirmResponse.next(false)
this.confirmService.confirmResponse.next({ confirmed: false, value: this.inputValue })
}
})
}

View File

@ -61,6 +61,15 @@ export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
}
}
export const USER_OTP_TOKEN_VALIDATOR: BuildFormValidator = {
VALIDATORS: [
Validators.required
],
MESSAGES: {
required: $localize`OTP token is required.`
}
}
export const USER_PASSWORD_VALIDATOR = {
VALIDATORS: [
Validators.required,

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { AuthService, HtmlRendererService, Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { logger } from '@root-helpers/logger'
@ -29,7 +29,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private abuse: UserAbuse
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private htmlRenderer: HtmlRendererService,
private auth: AuthService,

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { AbuseService } from '@app/shared/shared-moderation'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -20,7 +20,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private notifier: Notifier,
private abuseService: AbuseService

View File

@ -0,0 +1,101 @@
import { Injectable } from '@angular/core'
import { AbstractControl, FormGroup } from '@angular/forms'
import { wait } from '@root-helpers/utils'
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
import { FormValidatorService } from './form-validator.service'
export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
export type FormReactiveValidationMessages = {
[ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
}
@Injectable()
export class FormReactiveService {
constructor (private formValidatorService: FormValidatorService) {
}
buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues)
form.statusChanges.subscribe(async () => {
// FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
await this.waitPendingCheck(form)
this.onStatusChanged({ form, formErrors, validationMessages })
})
return { form, formErrors, validationMessages }
}
async waitPendingCheck (form: FormGroup) {
if (form.status !== 'PENDING') return
// FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
// return firstValueFrom(form.statusChanges.pipe(filter(status => status !== 'PENDING')))
// So we have to fallback to active wait :/
do {
await wait(10)
} while (form.status === 'PENDING')
}
markAllAsDirty (controlsArg: { [ key: string ]: AbstractControl }) {
const controls = controlsArg
for (const key of Object.keys(controls)) {
const control = controls[key]
if (control instanceof FormGroup) {
this.markAllAsDirty(control.controls)
continue
}
control.markAsDirty()
}
}
forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveValidationMessages) {
this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false })
}
private onStatusChanged (options: {
form: FormGroup
formErrors: FormReactiveErrors
validationMessages: FormReactiveValidationMessages
onlyDirty?: boolean // default true
}) {
const { form, formErrors, validationMessages, onlyDirty = true } = options
for (const field of Object.keys(formErrors)) {
if (formErrors[field] && typeof formErrors[field] === 'object') {
this.onStatusChanged({
form: form.controls[field] as FormGroup,
formErrors: formErrors[field] as FormReactiveErrors,
validationMessages: validationMessages[field] as FormReactiveValidationMessages,
onlyDirty
})
continue
}
// clear previous error message (if any)
formErrors[field] = ''
const control = form.get(field)
if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
const staticMessages = validationMessages[field]
for (const key of Object.keys(control.errors)) {
const formErrorValue = control.errors[key]
// Try to find error message in static validation messages first
// Then check if the validator returns a string that is the error
if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
else throw new Error('Form error value of ' + field + ' is invalid')
}
}
}
}

View File

@ -1,16 +1,9 @@
import { AbstractControl, FormGroup } from '@angular/forms'
import { wait } from '@root-helpers/utils'
import { FormGroup } from '@angular/forms'
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
import { FormValidatorService } from './form-validator.service'
export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
export type FormReactiveValidationMessages = {
[ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
}
import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service'
export abstract class FormReactive {
protected abstract formValidatorService: FormValidatorService
protected abstract formReactiveService: FormReactiveService
protected formChanged = false
form: FormGroup
@ -18,86 +11,22 @@ export abstract class FormReactive {
validationMessages: FormReactiveValidationMessages
buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues)
const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues)
this.form = form
this.formErrors = formErrors
this.validationMessages = validationMessages
this.form.statusChanges.subscribe(async () => {
// FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
await this.waitPendingCheck()
this.onStatusChanged(this.form, this.formErrors, this.validationMessages)
})
}
protected async waitPendingCheck () {
if (this.form.status !== 'PENDING') return
// FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
// return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING')))
// So we have to fallback to active wait :/
do {
await wait(10)
} while (this.form.status === 'PENDING')
return this.formReactiveService.waitPendingCheck(this.form)
}
protected markAllAsDirty (controlsArg?: { [ key: string ]: AbstractControl }) {
const controls = controlsArg || this.form.controls
for (const key of Object.keys(controls)) {
const control = controls[key]
if (control instanceof FormGroup) {
this.markAllAsDirty(control.controls)
continue
}
control.markAsDirty()
}
protected markAllAsDirty () {
return this.formReactiveService.markAllAsDirty(this.form.controls)
}
protected forceCheck () {
this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
}
private onStatusChanged (
form: FormGroup,
formErrors: FormReactiveErrors,
validationMessages: FormReactiveValidationMessages,
onlyDirty = true
) {
for (const field of Object.keys(formErrors)) {
if (formErrors[field] && typeof formErrors[field] === 'object') {
this.onStatusChanged(
form.controls[field] as FormGroup,
formErrors[field] as FormReactiveErrors,
validationMessages[field] as FormReactiveValidationMessages,
onlyDirty
)
continue
}
// clear previous error message (if any)
formErrors[field] = ''
const control = form.get(field)
if (control.dirty) this.formChanged = true
if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
const staticMessages = validationMessages[field]
for (const key of Object.keys(control.errors)) {
const formErrorValue = control.errors[key]
// Try to find error message in static validation messages first
// Then check if the validator returns a string that is the error
if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
else throw new Error('Form error value of ' + field + ' is invalid')
}
}
return this.formReactiveService.forceCheck(this.form, this.formErrors, this.validationMessages)
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'
import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive'
import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service'
@Injectable()
export class FormValidatorService {

View File

@ -1,4 +1,5 @@
export * from './advanced-input-filter.component'
export * from './form-reactive.service'
export * from './form-reactive'
export * from './form-validator.service'
export * from './form-validator.service'

View File

@ -1,4 +1,4 @@
import { Component, forwardRef, Input } from '@angular/core'
import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Notifier } from '@app/core'
@ -15,6 +15,8 @@ import { Notifier } from '@app/core'
]
})
export class InputTextComponent implements ControlValueAccessor {
@ViewChild('input') inputElement: ElementRef
@Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined
@Input() value = ''
@Input() autocomplete = 'off'
@ -65,4 +67,10 @@ export class InputTextComponent implements ControlValueAccessor {
update () {
this.propagateChange(this.value)
}
focus () {
const el: HTMLElement = this.inputElement.nativeElement
el.focus({ preventScroll: true })
}
}

View File

@ -1,4 +1,3 @@
import { InputMaskModule } from 'primeng/inputmask'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@ -7,6 +6,7 @@ import { SharedGlobalIconModule } from '../shared-icons'
import { SharedMainModule } from '../shared-main/shared-main.module'
import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
import { DynamicFormFieldComponent } from './dynamic-form-field.component'
import { FormReactiveService } from './form-reactive.service'
import { FormValidatorService } from './form-validator.service'
import { InputSwitchComponent } from './input-switch.component'
import { InputTextComponent } from './input-text.component'
@ -96,7 +96,8 @@ import { TimestampInputComponent } from './timestamp-input.component'
],
providers: [
FormValidatorService
FormValidatorService,
FormReactiveService
]
})
export class SharedFormModule { }

View File

@ -27,13 +27,16 @@ export class AuthInterceptor implements HttpInterceptor {
.pipe(
catchError((err: HttpErrorResponse) => {
const error = err.error as PeerTubeProblemDocument
const isOTPMissingError = this.authService.isOTPMissingError(err)
if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
return this.handleTokenExpired(req, next)
}
if (!isOTPMissingError) {
if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
return this.handleTokenExpired(req, next)
}
if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
return this.handleNotAuthenticated(err)
if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
return this.handleNotAuthenticated(err)
}
}
return observableThrowError(() => err)

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators'
@ -18,7 +18,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal
) {
super()

View File

@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { Account } from '@app/shared/shared-main'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -26,7 +26,7 @@ export class AccountReportComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private abuseService: AbuseService,
private notifier: Notifier

View File

@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { VideoComment } from '@app/shared/shared-video-comment'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -27,7 +27,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private abuseService: AbuseService,
private notifier: Notifier

View File

@ -3,7 +3,7 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser'
import { Notifier } from '@app/core'
import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
@ -27,7 +27,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private abuseService: AbuseService,
private notifier: Notifier,

View File

@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { prepareIcu } from '@app/helpers'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { User } from '@shared/models'
@ -25,7 +25,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
modalMessage = ''
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private notifier: Notifier,
private userAdminService: UserAdminService,

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { prepareIcu } from '@app/helpers'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { Video } from '@app/shared/shared-main'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -25,7 +25,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private modalService: NgbModal,
private videoBlocklistService: VideoBlockService,
private notifier: Notifier

View File

@ -1,7 +1,7 @@
import { Subject, Subscription } from 'rxjs'
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models'
import { SelectOptionsItem } from 'src/types'
@ -22,7 +22,7 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
private serverConfig: HTMLServerConfig
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private userService: UserService,

View File

@ -3,7 +3,7 @@ import { Subject, Subscription } from 'rxjs'
import { first } from 'rxjs/operators'
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { UserUpdateMe } from '@shared/models'
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
@ -22,7 +22,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
formValuesWatcher: Subscription
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private userService: UserService,

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'
import { Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { logger } from '@root-helpers/logger'
import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators'
@ -15,7 +15,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
@Input() showHelp = false
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private notifier: Notifier
) {
super()

View File

@ -1,4 +1,5 @@
export * from './user-admin.service'
export * from './user-signup.service'
export * from './two-factor.service'
export * from './shared-users.module'

View File

@ -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 { }

View File

@ -0,0 +1,52 @@
import { catchError } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { RestExtractor, UserService } from '@app/core'
import { TwoFactorEnableResult } from '@shared/models'
@Injectable()
export class TwoFactorService {
constructor (
private authHttp: HttpClient,
private restExtractor: RestExtractor
) { }
// ---------------------------------------------------------------------------
requestTwoFactor (options: {
userId: number
currentPassword: string
}) {
const { userId, currentPassword } = options
const url = UserService.BASE_USERS_URL + userId + '/two-factor/request'
return this.authHttp.post<TwoFactorEnableResult>(url, { currentPassword })
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
confirmTwoFactorRequest (options: {
userId: number
requestToken: string
otpToken: string
}) {
const { userId, requestToken, otpToken } = options
const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request'
return this.authHttp.post(url, { requestToken, otpToken })
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
disableTwoFactor (options: {
userId: number
currentPassword?: string
}) {
const { userId, currentPassword } = options
const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable'
return this.authHttp.post(url, { currentPassword })
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
}

View File

@ -3,7 +3,7 @@ import { Subject, Subscription } from 'rxjs'
import { debounceTime, filter } from 'rxjs/operators'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
import { secondsToTime } from '@shared/core-utils'
import {
Video,
@ -59,7 +59,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
private pendingAddId: number
constructor (
protected formValidatorService: FormValidatorService,
protected formReactiveService: FormReactiveService,
private authService: AuthService,
private notifier: Notifier,
private videoPlaylistService: VideoPlaylistService,

View File

@ -3,32 +3,32 @@
@import './_bootstrap-variables';
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import '~bootstrap/scss/maps';
@import '~bootstrap/scss/mixins';
@import '~bootstrap/scss/utilities';
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/maps';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/utilities';
@import '~bootstrap/scss/root';
@import '~bootstrap/scss/reboot';
@import '~bootstrap/scss/type';
@import '~bootstrap/scss/grid';
@import '~bootstrap/scss/forms';
@import '~bootstrap/scss/buttons';
@import '~bootstrap/scss/dropdown';
@import '~bootstrap/scss/button-group';
@import '~bootstrap/scss/nav';
@import '~bootstrap/scss/card';
@import '~bootstrap/scss/accordion';
@import '~bootstrap/scss/alert';
@import '~bootstrap/scss/close';
@import '~bootstrap/scss/modal';
@import '~bootstrap/scss/tooltip';
@import '~bootstrap/scss/popover';
@import '~bootstrap/scss/spinners';
@import 'bootstrap/scss/root';
@import 'bootstrap/scss/reboot';
@import 'bootstrap/scss/type';
@import 'bootstrap/scss/grid';
@import 'bootstrap/scss/forms';
@import 'bootstrap/scss/buttons';
@import 'bootstrap/scss/dropdown';
@import 'bootstrap/scss/button-group';
@import 'bootstrap/scss/nav';
@import 'bootstrap/scss/card';
@import 'bootstrap/scss/accordion';
@import 'bootstrap/scss/alert';
@import 'bootstrap/scss/close';
@import 'bootstrap/scss/modal';
@import 'bootstrap/scss/tooltip';
@import 'bootstrap/scss/popover';
@import 'bootstrap/scss/spinners';
@import '~bootstrap/scss/helpers';
@import '~bootstrap/scss/utilities/api';
@import 'bootstrap/scss/helpers';
@import 'bootstrap/scss/utilities/api';
.accordion {
--bs-accordion-color: #{pvar(--mainForegroundColor)};

View File

@ -1,6 +1,6 @@
@use 'sass:math';
@use 'sass:color';
@use '~bootstrap/scss/functions' as *;
@use 'bootstrap/scss/functions' as *;
$small-view: 800px;
$mobile-view: 500px;

View File

@ -15,7 +15,7 @@ $ng-select-height: 30px;
$ng-select-value-padding-left: 15px;
$ng-select-value-font-size: $form-input-font-size;
@import '~@ng-select/ng-select/scss/default.theme';
@import '@ng-select/ng-select/scss/default.theme';
.ng-select {
font-size: $ng-select-value-font-size;

View File

@ -1,4 +1,4 @@
@use '~bootstrap/scss/functions' as *;
@use 'bootstrap/scss/functions' as *;
$primary-foreground-color: #fff;
$primary-foreground-opacity: 0.9;

View File

@ -10,6 +10,11 @@ webserver:
hostname: 'localhost'
port: 9000
# Secrets you need to generate the first time you run PeerTube
secrets:
# Generate one using `openssl rand -hex 32`
peertube: ''
rates_limit:
api:
# 50 attempts in 10 seconds

View File

@ -5,6 +5,9 @@ listen:
webserver:
https: false
secrets:
peertube: 'my super dev secret'
database:
hostname: 'localhost'
port: 5432

View File

@ -8,6 +8,11 @@ webserver:
hostname: 'example.com'
port: 443
# Secrets you need to generate the first time you run PeerTube
secret:
# Generate one using `openssl rand -hex 32`
peertube: ''
rates_limit:
api:
# 50 attempts in 10 seconds

View File

@ -5,6 +5,9 @@ listen:
webserver:
https: false
secrets:
peertube: 'my super secret'
rates_limit:
signup:
window: 10 minutes

View File

@ -147,6 +147,7 @@
"node-media-server": "^2.1.4",
"nodemailer": "^6.0.0",
"opentelemetry-instrumentation-sequelize": "^0.29.0",
"otpauth": "^8.0.3",
"p-queue": "^6",
"parse-torrent": "^9.1.0",
"password-generator": "^2.0.2",

View File

@ -45,7 +45,12 @@ try {
import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init'
checkConfig()
try {
checkConfig()
} catch (err) {
logger.error('Config error.', { err })
process.exit(-1)
}
// Trust our proxy (IP forwarding...)
app.set('trust proxy', CONFIG.TRUST_PROXY)

View File

@ -51,6 +51,7 @@ import { myVideosHistoryRouter } from './my-history'
import { myNotificationsRouter } from './my-notifications'
import { mySubscriptionsRouter } from './my-subscriptions'
import { myVideoPlaylistsRouter } from './my-video-playlists'
import { twoFactorRouter } from './two-factor'
const auditLogger = auditLoggerFactory('users')
@ -66,6 +67,7 @@ const askSendEmailLimiter = buildRateLimiter({
})
const usersRouter = express.Router()
usersRouter.use('/', twoFactorRouter)
usersRouter.use('/', tokensRouter)
usersRouter.use('/', myNotificationsRouter)
usersRouter.use('/', mySubscriptionsRouter)

View File

@ -1,8 +1,9 @@
import express from 'express'
import { logger } from '@server/helpers/logger'
import { CONFIG } from '@server/initializers/config'
import { OTP } from '@server/initializers/constants'
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
import { handleOAuthToken } from '@server/lib/auth/oauth'
import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth'
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
import { Hooks } from '@server/lib/plugins/hooks'
import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
@ -79,6 +80,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e
} 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,

View File

@ -0,0 +1,95 @@
import express from 'express'
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
import { encrypt } from '@server/helpers/peertube-crypto'
import { CONFIG } from '@server/initializers/config'
import { Redis } from '@server/lib/redis'
import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
import {
confirmTwoFactorValidator,
disableTwoFactorValidator,
requestOrConfirmTwoFactorValidator
} from '@server/middlewares/validators/two-factor'
import { HttpStatusCode, TwoFactorEnableResult } from '@shared/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)
}

View File

@ -6,7 +6,7 @@
*/
import { exec, ExecOptions } from 'child_process'
import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions } from 'crypto'
import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto'
import { truncate } from 'lodash'
import { pipeline } from 'stream'
import { URL } from 'url'
@ -311,7 +311,17 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
}
}
// eslint-disable-next-line max-len
function promisify3<T, U, V, A> (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise<A> {
return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ])
})
}
}
const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
const scryptPromise = promisify3<string, string, number, Buffer>(scrypt)
const execPromise2 = promisify2<string, any, string>(exec)
const execPromise = promisify1<string, string>(exec)
const pipelinePromise = promisify(pipeline)
@ -339,6 +349,8 @@ export {
promisify1,
promisify2,
scryptPromise,
randomBytesPromise,
generateRSAKeyPairPromise,

58
server/helpers/otp.ts Normal file
View File

@ -0,0 +1,58 @@
import { Secret, TOTP } from 'otpauth'
import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants'
import { decrypt } from './peertube-crypto'
async function isOTPValid (options: {
encryptedSecret: string
token: string
}) {
const { token, encryptedSecret } = options
const secret = await decrypt(encryptedSecret, CONFIG.SECRETS.PEERTUBE)
const totp = new TOTP({
...baseOTPOptions(),
secret
})
const delta = totp.validate({
token,
window: 1
})
if (delta === null) return false
return true
}
function generateOTPSecret (email: string) {
const totp = new TOTP({
...baseOTPOptions(),
label: email,
secret: new Secret()
})
return {
secret: totp.secret.base32,
uri: totp.toString()
}
}
export {
isOTPValid,
generateOTPSecret
}
// ---------------------------------------------------------------------------
function baseOTPOptions () {
return {
issuer: WEBSERVER.HOST,
algorithm: 'SHA1',
digits: 6,
period: 30
}
}

View File

@ -1,11 +1,11 @@
import { compare, genSalt, hash } from 'bcrypt'
import { createSign, createVerify } from 'crypto'
import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto'
import { Request } from 'express'
import { cloneDeep } from 'lodash'
import { sha256 } from '@shared/extra-utils'
import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
import { MActor } from '../types/models'
import { generateRSAKeyPairPromise, promisify1, promisify2 } from './core-utils'
import { generateRSAKeyPairPromise, promisify1, promisify2, randomBytesPromise, scryptPromise } from './core-utils'
import { jsonld } from './custom-jsonld-signature'
import { logger } from './logger'
@ -21,9 +21,13 @@ function createPrivateAndPublicKeys () {
return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE)
}
// ---------------------------------------------------------------------------
// User password checks
// ---------------------------------------------------------------------------
function comparePassword (plainPassword: string, hashPassword: string) {
if (!plainPassword) return Promise.resolve(false)
return bcryptComparePromise(plainPassword, hashPassword)
}
@ -33,7 +37,9 @@ async function cryptPassword (password: string) {
return bcryptHashPromise(password, salt)
}
// ---------------------------------------------------------------------------
// HTTP Signature
// ---------------------------------------------------------------------------
function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
@ -62,7 +68,9 @@ function parseHTTPSignature (req: Request, clockSkew?: number) {
return parsed
}
// ---------------------------------------------------------------------------
// JSONLD
// ---------------------------------------------------------------------------
function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
@ -112,12 +120,42 @@ async function signJsonLDObject <T> (byActor: MActor, data: T) {
return Object.assign(data, { signature })
}
// ---------------------------------------------------------------------------
function buildDigest (body: any) {
const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
return 'SHA-256=' + sha256(rawBody, 'base64')
}
// ---------------------------------------------------------------------------
// Encryption
// ---------------------------------------------------------------------------
async function encrypt (str: string, secret: string) {
const iv = await randomBytesPromise(ENCRYPTION.IV)
const key = await scryptPromise(secret, ENCRYPTION.SALT, 32)
const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv)
let encrypted = iv.toString(ENCRYPTION.ENCODING) + ':'
encrypted += cipher.update(str, 'utf8', ENCRYPTION.ENCODING)
encrypted += cipher.final(ENCRYPTION.ENCODING)
return encrypted
}
async function decrypt (encryptedArg: string, secret: string) {
const [ ivStr, encryptedStr ] = encryptedArg.split(':')
const iv = Buffer.from(ivStr, 'hex')
const key = await scryptPromise(secret, ENCRYPTION.SALT, 32)
const decipher = createDecipheriv(ENCRYPTION.ALGORITHM, key, iv)
return decipher.update(encryptedStr, ENCRYPTION.ENCODING, 'utf8') + decipher.final('utf8')
}
// ---------------------------------------------------------------------------
export {
@ -129,7 +167,10 @@ export {
comparePassword,
createPrivateAndPublicKeys,
cryptPassword,
signJsonLDObject
signJsonLDObject,
encrypt,
decrypt
}
// ---------------------------------------------------------------------------

View File

@ -42,6 +42,7 @@ function checkConfig () {
logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.')
}
checkSecretsConfig()
checkEmailConfig()
checkNSFWPolicyConfig()
checkLocalRedundancyConfig()
@ -103,6 +104,12 @@ export {
// ---------------------------------------------------------------------------
function checkSecretsConfig () {
if (!CONFIG.SECRETS.PEERTUBE) {
throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`')
}
}
function checkEmailConfig () {
if (!isEmailEnabled()) {
if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {

View File

@ -11,6 +11,7 @@ const config: IConfig = require('config')
function checkMissedConfig () {
const required = [ 'listen.port', 'listen.hostname',
'webserver.https', 'webserver.hostname', 'webserver.port',
'secrets.peertube',
'trust_proxy',
'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',

View File

@ -20,6 +20,9 @@ const CONFIG = {
PORT: config.get<number>('listen.port'),
HOSTNAME: config.get<string>('listen.hostname')
},
SECRETS: {
PEERTUBE: config.get<string>('secrets.peertube')
},
DATABASE: {
DBNAME: config.has('database.name') ? config.get<string>('database.name') : 'peertube' + config.get<string>('database.suffix'),
HOSTNAME: config.get<string>('database.hostname'),

View File

@ -1,5 +1,5 @@
import { RepeatOptions } from 'bullmq'
import { randomBytes } from 'crypto'
import { Encoding, randomBytes } from 'crypto'
import { invert } from 'lodash'
import { join } from 'path'
import { randomInt, root } from '@shared/core-utils'
@ -25,7 +25,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 740
const LAST_MIGRATION_VERSION = 745
// ---------------------------------------------------------------------------
@ -637,9 +637,18 @@ let PRIVATE_RSA_KEY_SIZE = 2048
// Password encryption
const BCRYPT_SALT_SIZE = 10
const ENCRYPTION = {
ALGORITHM: 'aes-256-cbc',
IV: 16,
SALT: 'peertube',
ENCODING: 'hex' as Encoding
}
const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
@ -805,6 +814,10 @@ const REDUNDANCY = {
}
const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
const OTP = {
HEADER_NAME: 'x-peertube-otp',
HEADER_REQUIRED_VALUE: 'required; app'
}
const ASSETS_PATH = {
DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-audio-background.jpg'),
@ -953,6 +966,7 @@ const VIDEO_FILTERS = {
export {
WEBSERVER,
API_VERSION,
ENCRYPTION,
VIDEO_LIVE,
PEERTUBE_VERSION,
LAZY_STATIC_PATHS,
@ -986,6 +1000,7 @@ export {
FOLLOW_STATES,
DEFAULT_USER_THEME_NAME,
SERVER_ACTOR_NAME,
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
PLUGIN_GLOBAL_CSS_FILE_NAME,
PLUGIN_GLOBAL_CSS_PATH,
PRIVATE_RSA_KEY_SIZE,
@ -1041,6 +1056,7 @@ export {
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
ASSETS_PATH,
FILES_CONTENT_HASH,
OTP,
loadLanguages,
buildLanguages,
generateContentHash

Some files were not shown because too many files have changed in this diff Show More