allow administration to change/reset a user's password
This commit is contained in:
parent
c7ca4c8be7
commit
328c78bc4a
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './user-create.component'
|
||||
export * from './user-update.component'
|
||||
export * from './user-password.component'
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -44,7 +44,8 @@ export const UsersRoutes: Routes = [
|
|||
data: {
|
||||
meta: {
|
||||
title: 'Update a user'
|
||||
}
|
||||
},
|
||||
isAdministration: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue