"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"
}
}
<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>
\ No newline at end of file
+ <div class="form-group">
+ <label i18n>Manually set the user password</label>
+ <my-user-password [userId]="userId"></my-user-password>
+ </div>
+</div>
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;
+ }
+ }
+}
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
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
<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">
input:not([type=submit]):not([type=checkbox]) {
@include peertube-input-text(340px);
+
display: block;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
-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({
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
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
) {
})
}
- ngOnDestroy () {
- //
- }
-
formValidated () {
this.error = undefined
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 })
)
},
)
}
- 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, 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'
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 })
)
},
)
}
- 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 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
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()
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
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)
}
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()
}
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
CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
+
+ RATES_LIMIT.LOGIN.MAX = 20
}
updateWebserverUrls()
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()
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` +
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'),
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)
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
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)
})
import { UserRole } from './user-role'
export interface UserUpdate {
+ password?: string
email?: string
emailVerified?: boolean
videoQuota?: number
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