Merge branch 'feature/otp' into develop
This commit is contained in:
commit
63fa260a81
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './my-account-two-factor-button.component'
|
||||
export * from './my-account-two-factor.component'
|
|
@ -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>
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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})!`,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './user-admin.service'
|
||||
export * from './user-signup.service'
|
||||
export * from './two-factor.service'
|
||||
|
||||
export * from './shared-users.module'
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '~bootstrap/scss/functions' as *;
|
||||
@use 'bootstrap/scss/functions' as *;
|
||||
|
||||
$primary-foreground-color: #fff;
|
||||
$primary-foreground-opacity: 0.9;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,9 @@ listen:
|
|||
webserver:
|
||||
https: false
|
||||
|
||||
secrets:
|
||||
peertube: 'my super dev secret'
|
||||
|
||||
database:
|
||||
hostname: 'localhost'
|
||||
port: 5432
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,9 @@ listen:
|
|||
webserver:
|
||||
https: false
|
||||
|
||||
secrets:
|
||||
peertube: 'my super secret'
|
||||
|
||||
rates_limit:
|
||||
signup:
|
||||
window: 10 minutes
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue