"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
"whatwg-fetch": "^3.0.0",
"zone.js": "~0.8.5"
+ },
+ "dependencies": {
+ "generate-password-browser": "^1.0.2"
}
}
import { JobsComponent } from './jobs/job.component'
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
import { JobService } from './jobs/shared/job.service'
-import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users'
+import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
UsersComponent,
UserCreateComponent,
UserUpdateComponent,
+ UserPasswordComponent,
UserListComponent,
ModerationComponent,
export * from './user-create.component'
export * from './user-update.component'
+export * from './user-password.component'
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>
+
+<div *ngIf="isAdministration">
+ <div class="account-title" i18n>Danger Zone</div>
+
+ <p i18n>Send a link to reset the password by mail to the user.</p>
+ <button (click)="resetPassword()" i18n>Ask for new password</button>
+
+ <p class="mt-4" i18n>Manually set the user password</p>
+ <my-user-password></my-user-password>
+</div>
\ No newline at end of file
@include peertube-select-container(340px);
}
-input[type=submit] {
+input[type=submit], button {
@include peertube-button;
@include orange-button;
margin-top: 5px;
font-size: 11px;
}
+
+.account-title {
+ @include in-content-small-title;
+
+ margin-top: 55px;
+ margin-bottom: 30px;
+}
--- /dev/null
+<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
+ <div class="form-group">
+
+ <div class="input-group mb-3">
+ <div class="input-group-prepend">
+ <div class="input-group-text">
+ <input type="checkbox" aria-label="Show password" (change)="togglePasswordVisibility()">
+ </div>
+ </div>
+ <input id="passwordField" #passwordField
+ [attr.type]="showPassword ? 'text' : 'password'" id="password"
+ formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
+ >
+ <div class="input-group-append">
+ <button class="btn btn-sm btn-outline-secondary" (click)="generatePassword() "
+ type="button">Generate</button>
+ </div>
+ </div>
+ <div *ngIf="formErrors.password" class="form-error">
+ {{ formErrors.password }}
+ </div>
+ </div>
+
+ <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
+</form>
\ No newline at end of file
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input:not([type=submit]):not([type=checkbox]) {
+ @include peertube-input-text(340px);
+ display: block;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right: none;
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+
+ margin-top: 10px;
+}
+
+.input-group-append {
+ height: 30px;
+}
--- /dev/null
+import { Component, OnDestroy, OnInit, Input } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Subscription } from 'rxjs'
+import * as generator from 'generate-password-browser'
+import { NotificationsService } from 'angular2-notifications'
+import { UserService } from '@app/shared/users/user.service'
+import { ServerService } from '../../../core'
+import { User, UserUpdate } from '../../../../../../shared'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
+import { ConfigService } from '@app/+admin/config/shared/config.service'
+import { FormReactive } from '../../../shared'
+
+@Component({
+ selector: 'my-user-password',
+ templateUrl: './user-password.component.html',
+ styleUrls: [ './user-password.component.scss' ]
+})
+export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy {
+ error: string
+ userId: number
+ username: string
+ showPassword = false
+
+ private paramsSub: Subscription
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ protected serverService: ServerService,
+ protected configService: ConfigService,
+ private userValidatorsService: UserValidatorsService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private notificationsService: NotificationsService,
+ private userService: UserService,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ ngOnInit () {
+ this.buildForm({
+ password: this.userValidatorsService.USER_PASSWORD
+ })
+
+ this.paramsSub = this.route.params.subscribe(routeParams => {
+ const userId = routeParams['id']
+ this.userService.getUser(userId).subscribe(
+ user => this.onUserFetched(user),
+
+ err => this.error = err.message
+ )
+ })
+ }
+
+ ngOnDestroy () {
+ this.paramsSub.unsubscribe()
+ }
+
+ formValidated () {
+ this.error = undefined
+
+ const userUpdate: UserUpdate = this.form.value
+
+ this.userService.updateUser(this.userId, userUpdate).subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Password changed for user {{username}}.', { username: this.username })
+ )
+ },
+
+ err => this.error = err.message
+ )
+ }
+
+ generatePassword () {
+ this.form.patchValue({
+ password: generator.generate({
+ length: 16,
+ excludeSimilarCharacters: true,
+ strict: true
+ })
+ })
+ }
+
+ togglePasswordVisibility () {
+ this.showPassword = !this.showPassword
+ }
+
+ getFormButtonTitle () {
+ return this.i18n('Update user password')
+ }
+
+ private onUserFetched (userJson: User) {
+ this.userId = userJson.id
+ this.username = userJson.username
+ }
+}
-import { Component, OnDestroy, OnInit } from '@angular/core'
+import { Component, OnDestroy, OnInit, Input } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs'
import { Notifier } from '@app/core'
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
error: string
userId: number
+ userEmail: string
username: string
+ isAdministration = false
private paramsSub: Subscription
+ private isAdministrationSub: Subscription
constructor (
protected formValidatorService: FormValidatorService,
err => this.error = err.message
)
})
+
+ this.isAdministrationSub = this.route.data.subscribe(data => {
+ if (data.isAdministration) this.isAdministration = data.isAdministration
+ })
}
ngOnDestroy () {
this.paramsSub.unsubscribe()
+ this.isAdministrationSub.unsubscribe()
}
formValidated () {
return this.i18n('Update user')
}
+ resetPassword () {
+ this.userService.askResetPassword(this.userEmail).subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
+ )
+ },
+
+ err => this.error = err.message
+ )
+ }
+
private onUserFetched (userJson: User) {
this.userId = userJson.id
this.username = userJson.username
+ this.userEmail = userJson.email
this.form.patchValue({
email: userJson.email,
data: {
meta: {
title: 'Update a user'
- }
+ },
+ isAdministration: true
}
}
]
)
}
+ resetUserPassword (userId: number) {
+ return this.authHttp.post(UserService.BASE_USERS_URL + userId + '/reset-password', {})
+ .pipe(catchError(err => this.restExtractor.handleError(err)))
+ }
+
verifyEmail (userId: number, verificationString: string) {
const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
const body = {
import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
+import { pseudoRandomBytesPromise } from '../../../helpers/core-utils'
import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis'
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addForceResetPasswordEmailJob (to: string, resetPasswordUrl: string) {
+ const text = `Hi dear user,\n\n` +
+ `Your password has been reset on ${CONFIG.WEBSERVER.HOST}! ` +
+ `Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
+ `Cheers,\n` +
+ `PeerTube.`
+
+ const emailPayload: EmailPayload = {
+ to: [ to ],
+ subject: 'Reset of your PeerTube password',
+ text
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') {
const followerName = actorFollow.ActorFollower.Account.getDisplayName()
const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()