1
0
Fork 0

Cleanup reset user password by admin

And add some tests
This commit is contained in:
Chocobozzz 2019-02-11 09:30:29 +01:00
parent 67b1d3fed7
commit b426edd485
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
18 changed files with 97 additions and 85 deletions

View File

@ -164,7 +164,6 @@
"webpack-cli": "^3.0.8",
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
"whatwg-fetch": "^3.0.0",
"zone.js": "~0.8.5",
"generate-password-browser": "^1.0.2"
"zone.js": "~0.8.5"
}
}

View File

@ -82,12 +82,16 @@
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>
<div *ngIf="!isCreation()">
<div *ngIf="!isCreation()" class="danger-zone">
<div class="account-title" i18n>Danger Zone</div>
<p i18n>Send a link to reset the password by mail to the user.</p>
<button style="margin-top:0;" (click)="resetPassword()" i18n>Ask for new password</button>
<div class="form-group reset-password-email">
<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>
<p class="mt-4" i18n>Manually set the user password</p>
<my-user-password userId="userId"></my-user-password>
</div>
<div class="form-group">
<label i18n>Manually set the user password</label>
<my-user-password [userId]="userId"></my-user-password>
</div>
</div>

View File

@ -32,3 +32,16 @@ input[type=submit], button {
margin-top: 55px;
margin-bottom: 30px;
}
.danger-zone {
.reset-password-email {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
button {
display: block;
margin-top: 0;
}
}
}

View File

@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive {
videoQuotaDailyOptions: { value: string, label: string }[] = []
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
username: string
userId: number
protected abstract serverService: ServerService
protected abstract configService: ConfigService
@ -37,6 +38,10 @@ export abstract class UserEdit extends FormReactive {
return multiplier * parseInt(this.form.value['videoQuota'], 10)
}
resetPassword () {
return
}
protected buildQuotaOptions () {
// These are used by a HTML select, so convert key into strings
this.videoQuotaOptions = this.configService

View File

@ -1,19 +1,15 @@
<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"
<div class="input-group">
<input id="password" [attr.type]="showPassword ? 'text' : '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>
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
<ng-container *ngIf="!showPassword" i18n>Show</ng-container>
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
</button>
</div>
</div>
<div *ngIf="formErrors.password" class="form-error">

View File

@ -3,6 +3,7 @@
input:not([type=submit]):not([type=checkbox]) {
@include peertube-input-text(340px);
display: block;
border-top-right-radius: 0;
border-bottom-right-radius: 0;

View File

@ -1,14 +1,11 @@
import { Component, OnDestroy, OnInit, Input } from '@angular/core'
import { Component, Input, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
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 { Notifier } 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({
@ -16,7 +13,7 @@ import { FormReactive } from '../../../shared'
templateUrl: './user-password.component.html',
styleUrls: [ './user-password.component.scss' ]
})
export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy {
export class UserPasswordComponent extends FormReactive implements OnInit {
error: string
username: string
showPassword = false
@ -25,12 +22,10 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
constructor (
protected formValidatorService: FormValidatorService,
protected serverService: ServerService,
protected configService: ConfigService,
private userValidatorsService: UserValidatorsService,
private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private notifier: Notifier,
private userService: UserService,
private i18n: I18n
) {
@ -43,10 +38,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
})
}
ngOnDestroy () {
//
}
formValidated () {
this.error = undefined
@ -54,8 +45,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
this.userService.updateUser(this.userId, userUpdate).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.notifier.success(
this.i18n('Password changed for user {{username}}.', { username: this.username })
)
},
@ -64,16 +54,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
)
}
generatePassword () {
this.form.patchValue({
password: generator.generate({
length: 16,
excludeSimilarCharacters: true,
strict: true
})
})
}
togglePasswordVisibility () {
this.showPassword = !this.showPassword
}
@ -81,9 +61,4 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
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, Input } from '@angular/core'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs'
import { Notifier } from '@app/core'
@ -93,8 +93,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
resetPassword () {
this.userService.askResetPassword(this.userEmail).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.notifier.success(
this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
)
},

View File

@ -103,11 +103,6 @@ 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,7 +3,6 @@ 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'
@ -230,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
return res.status(204).end()
}
async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
async function blockUser (req: express.Request, res: express.Response) {
const user: UserModel = res.locals.user
const reason = req.body.reason
@ -239,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp
return res.status(204).end()
}
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
function getUser (req: express.Request, res: express.Response) {
return res.json((res.locals.user as UserModel).toFormattedJSON())
}
async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) {
async function autocompleteUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.autoComplete(req.query.search as string)
return res.json(resultList)
}
async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) {
async function listUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) {
async function removeUser (req: express.Request, res: express.Response) {
const user: UserModel = res.locals.user
await user.destroy()
@ -265,12 +264,13 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
return res.sendStatus(204)
}
async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
async function updateUser (req: express.Request, res: express.Response) {
const body: UserUpdate = req.body
const userToUpdate = res.locals.user as UserModel
const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
if (body.password !== undefined) userToUpdate.password = body.password
if (body.email !== undefined) userToUpdate.email = body.email
if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
@ -280,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
const user = await userToUpdate.save()
// Destroy user token to refresh rights
if (roleChanged) await deleteUserToken(userToUpdate.id)
if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
// Don't need to send this update to followers, these attributes are not propagated
// Don't need to send this update to followers, these attributes are not federated
return res.sendStatus(204)
}
@ -294,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response
const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
await Emailer.Instance.addForgetPasswordEmailJob(user.email, url)
await Emailer.Instance.addPasswordResetEmailJob(user.email, url)
return res.status(204).end()
}

View File

@ -167,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) {
return res.sendStatus(204)
}
async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) {
async function updateMe (req: express.Request, res: express.Response) {
const body: UserUpdateMe = req.body
const user: UserModel = res.locals.oauth.token.user

View File

@ -711,6 +711,8 @@ if (isTestInstance() === true) {
CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
RATES_LIMIT.LOGIN.MAX = 20
}
updateWebserverUrls()

View File

@ -101,22 +101,6 @@ 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()
@ -312,9 +296,9 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) {
addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
const text = `Hi dear user,\n\n` +
`It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` +
`A reset password procedure for your account ${to} has been requested on ${CONFIG.WEBSERVER.HOST} ` +
`Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
`If you are not the person who initiated this request, please ignore this email.\n\n` +
`Cheers,\n` +

View File

@ -113,6 +113,7 @@ const deleteMeValidator = [
const usersUpdateValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
@ -233,6 +234,7 @@ const usersAskResetPasswordValidator = [
logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
const exists = await checkUserEmailExist(req.body.email, res, false)
if (!exists) {
logger.debug('User with email %s does not exist (asking reset password).', req.body.email)

View File

@ -464,6 +464,24 @@ describe('Test users API validators', function () {
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
})
it('Should fail with a too small password', async function () {
const fields = {
currentPassword: 'my super password',
password: 'bla'
}
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
})
it('Should fail with a too long password', async function () {
const fields = {
currentPassword: 'my super password',
password: 'super'.repeat(61)
}
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
})
it('Should fail with an non authenticated user', async function () {
const fields = {
videoQuota: 42

View File

@ -501,6 +501,22 @@ describe('Test users', function () {
accessTokenUser = await userLogin(server, user)
})
it('Should be able to update another user password', async function () {
await updateUser({
url: server.url,
userId,
accessToken,
password: 'password updated'
})
await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401)
await userLogin(server, user, 400)
user.password = 'password updated'
accessTokenUser = await userLogin(server, user)
})
it('Should be able to list video blacklist by a moderator', async function () {
await getBlacklistedVideosList(server.url, accessTokenUser)
})

View File

@ -1,6 +1,7 @@
import { UserRole } from './user-role'
export interface UserUpdate {
password?: string
email?: string
emailVerified?: boolean
videoQuota?: number

View File

@ -213,11 +213,13 @@ function updateUser (options: {
emailVerified?: boolean,
videoQuota?: number,
videoQuotaDaily?: number,
password?: string,
role?: UserRole
}) {
const path = '/api/v1/users/' + options.userId
const toSend = {}
if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota