]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
allow administration to change/reset a user's password
authorRigel Kent <sendmemail@rigelk.eu>
Sat, 6 Oct 2018 11:54:00 +0000 (13:54 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 11 Feb 2019 08:26:29 +0000 (09:26 +0100)
13 files changed:
client/package.json
client/src/app/+admin/admin.module.ts
client/src/app/+admin/users/user-edit/index.ts
client/src/app/+admin/users/user-edit/user-edit.component.html
client/src/app/+admin/users/user-edit/user-edit.component.scss
client/src/app/+admin/users/user-edit/user-password.component.html [new file with mode: 0644]
client/src/app/+admin/users/user-edit/user-password.component.scss [new file with mode: 0644]
client/src/app/+admin/users/user-edit/user-password.component.ts [new file with mode: 0644]
client/src/app/+admin/users/user-edit/user-update.component.ts
client/src/app/+admin/users/users.routes.ts
client/src/app/shared/users/user.service.ts
server/controllers/api/users/index.ts
server/lib/emailer.ts

index 3eea661f1053fff3eca38e3e8e6d146cd36b80c1..5f957bf7519e24059de8477da965ae3b0879f6c5 100644 (file)
     "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
     "whatwg-fetch": "^3.0.0",
     "zone.js": "~0.8.5"
+  },
+  "dependencies": {
+    "generate-password-browser": "^1.0.2"
   }
 }
index c06ae1d603c6613ad6bf89b44dd422a96ac74bf3..f7f347105171f00028b7c1f5f2a8a0e28e62d790 100644 (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,
index fd80a02e091ab4ad44d0da4c4ad6fd0fd4804900..ec734ef923760128eb6c8137f8b432f6649d8a41 100644 (file)
@@ -1,2 +1,3 @@
 export * from './user-create.component'
 export * from './user-update.component'
+export * from './user-password.component'
index 56cf7d17da3046bc68193f53f5279473812ac912..cbc06c157a26d1f90a6f1aad2e4250f4edbf1ea1 100644 (file)
 
   <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
index 6675f65cc7870a7ccdfafc37c057c0fc7edba14e..2b4aae83cd706ed47b6882d2ea6a1ce28671bbea 100644 (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;
+}
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html
new file mode 100644 (file)
index 0000000..ee7d8df
--- /dev/null
@@ -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>
\ No newline at end of file
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss
new file mode 100644 (file)
index 0000000..9185e78
--- /dev/null
@@ -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;
+}
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts
new file mode 100644 (file)
index 0000000..1f9ccb4
--- /dev/null
@@ -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
+  }
+}
index 61e64182391173e4674329c66b9157175860e7c3..cb74897d0d05d121b754d0e5065b26c03fc76a4c 100644 (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,
index 8b3791bd3fb21bc16dbc25132721b97e159874f7..460ebd89e8f09dff05ffb0914bb3e547f53ced29 100644 (file)
@@ -44,7 +44,8 @@ export const UsersRoutes: Routes = [
         data: {
           meta: {
             title: 'Update a user'
-          }
+          },
+          isAdministration: true
         }
       }
     ]
index cc5c051f173b040386ba9897a6541d0154da0c04..d0abc7def460a7ac9d7734d81eff2140622c714f 100644 (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 = {
index dbe0718d43efc6135974c662732c57db6eb36fd8..beac6d8b1bdca10570ffa5e2076f50ede446ae9a 100644 (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'
index f384a254e017d6912a71e8063ce2c25c19aa8512..7681164b3fc93eb4ddc9f8be4fb2fcdf8c3ddf9a 100644 (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()