allow administration to change/reset a user's password

pull/1625/head
Rigel Kent 2018-10-06 13:54:00 +02:00 committed by Chocobozzz
parent c7ca4c8be7
commit 328c78bc4a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
13 changed files with 217 additions and 4 deletions

View File

@ -165,5 +165,8 @@
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
"whatwg-fetch": "^3.0.0",
"zone.js": "~0.8.5"
},
"dependencies": {
"generate-password-browser": "^1.0.2"
}
}

View File

@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list.
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'
@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
UsersComponent,
UserCreateComponent,
UserUpdateComponent,
UserPasswordComponent,
UserListComponent,
ModerationComponent,

View File

@ -1,2 +1,3 @@
export * from './user-create.component'
export * from './user-update.component'
export * from './user-password.component'

View File

@ -81,3 +81,13 @@
<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>

View File

@ -14,7 +14,7 @@ input:not([type=submit]) {
@include peertube-select-container(340px);
}
input[type=submit] {
input[type=submit], button {
@include peertube-button;
@include orange-button;
@ -25,3 +25,10 @@ input[type=submit] {
margin-top: 5px;
font-size: 11px;
}
.account-title {
@include in-content-small-title;
margin-top: 55px;
margin-bottom: 30px;
}

View File

@ -0,0 +1,25 @@
<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>

View File

@ -0,0 +1,21 @@
@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;
}

View File

@ -0,0 +1,100 @@
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
}
}

View File

@ -1,4 +1,4 @@
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'
@ -19,9 +19,12 @@ import { UserService } from '@app/shared'
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,
@ -56,10 +59,15 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
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 () {
@ -89,9 +97,23 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
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,

View File

@ -44,7 +44,8 @@ export const UsersRoutes: Routes = [
data: {
meta: {
title: 'Update a user'
}
},
isAdministration: true
}
}
]

View File

@ -103,6 +103,11 @@ export class UserService {
)
}
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 = {

View File

@ -3,6 +3,7 @@ import * as RateLimit from 'express-rate-limit'
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'

View File

@ -101,6 +101,22 @@ class Emailer {
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()