Add ability to reset our password
This commit is contained in:
parent
80d1057bfc
commit
ecb4e35f4e
|
@ -7,7 +7,7 @@
|
|||
/test6/
|
||||
/storage/
|
||||
/config/production.yaml
|
||||
/config/local*.json
|
||||
/config/local*
|
||||
/ffmpeg/
|
||||
/*.sublime-project
|
||||
/*.sublime-workspace
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { ResetPasswordModule } from '@app/reset-password'
|
||||
|
||||
import { MetaModule, MetaLoader, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
|
||||
import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
|
||||
|
||||
import { AccountModule } from './account'
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
|
||||
import { AccountModule } from './account'
|
||||
import { CoreModule } from './core'
|
||||
import { LoginModule } from './login'
|
||||
import { SignupModule } from './signup'
|
||||
import { SharedModule } from './shared'
|
||||
import { VideosModule } from './videos'
|
||||
import { MenuComponent } from './menu'
|
||||
import { HeaderComponent } from './header'
|
||||
import { LoginModule } from './login'
|
||||
import { MenuComponent } from './menu'
|
||||
import { SharedModule } from './shared'
|
||||
import { SignupModule } from './signup'
|
||||
import { VideosModule } from './videos'
|
||||
|
||||
export function metaFactory (): MetaLoader {
|
||||
return new MetaStaticLoader({
|
||||
|
@ -46,6 +47,7 @@ export function metaFactory (): MetaLoader {
|
|||
AccountModule,
|
||||
CoreModule,
|
||||
LoginModule,
|
||||
ResetPasswordModule,
|
||||
SignupModule,
|
||||
SharedModule,
|
||||
VideosModule,
|
||||
|
|
|
@ -19,10 +19,13 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password" name="password" id="password" placeholder="Password" required
|
||||
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="password" name="password" id="password" placeholder="Password" required
|
||||
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||
>
|
||||
<div class="forgot-password-button" (click)="openForgotPasswordModal()">I forgot my password</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.password" class="form-error">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
|
@ -31,3 +34,36 @@
|
|||
<input type="submit" value="Login" [disabled]="!form.valid">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div bsModal #forgotPasswordModal="bs-modal" (onShown)="onForgotPasswordModalShown()" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span>
|
||||
<h4 class="modal-title">Forgot your password</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="forgot-password-email">Email</label>
|
||||
<input
|
||||
type="email" id="forgot-password-email" placeholder="Email address" required
|
||||
[(ngModel)]="forgotPasswordEmail" #forgotPasswordEmailInput
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<span class="action-button action-button-cancel" (click)="hideForgotPasswordModal()">
|
||||
Cancel
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="submit" value="Send me an email to reset my password" class="action-button-submit"
|
||||
(click)="askResetPassword()" [disabled]="!forgotPasswordEmailInput.validity.valid"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,3 +10,13 @@ input[type=submit] {
|
|||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
input[type=password] {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.forgot-password-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
import { UserService } from '@app/shared'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { ModalDirective } from 'ngx-bootstrap/modal'
|
||||
import { AuthService } from '../core'
|
||||
import { FormReactive } from '../shared'
|
||||
|
||||
|
@ -12,6 +14,9 @@ import { FormReactive } from '../shared'
|
|||
})
|
||||
|
||||
export class LoginComponent extends FormReactive implements OnInit {
|
||||
@ViewChild('forgotPasswordModal') forgotPasswordModal: ModalDirective
|
||||
@ViewChild('forgotPasswordEmailInput') forgotPasswordEmailInput: ElementRef
|
||||
|
||||
error: string = null
|
||||
|
||||
form: FormGroup
|
||||
|
@ -27,9 +32,12 @@ export class LoginComponent extends FormReactive implements OnInit {
|
|||
'required': 'Password is required.'
|
||||
}
|
||||
}
|
||||
forgotPasswordEmail = ''
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router
|
||||
) {
|
||||
|
@ -60,4 +68,29 @@ export class LoginComponent extends FormReactive implements OnInit {
|
|||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
askResetPassword () {
|
||||
this.userService.askResetPassword(this.forgotPasswordEmail)
|
||||
.subscribe(
|
||||
res => {
|
||||
const message = `An email with the reset password instructions will be sent to ${this.forgotPasswordEmail}.`
|
||||
this.notificationsService.success('Success', message)
|
||||
this.hideForgotPasswordModal()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error('Error', err.message)
|
||||
)
|
||||
}
|
||||
|
||||
onForgotPasswordModalShown () {
|
||||
this.forgotPasswordEmailInput.nativeElement.focus()
|
||||
}
|
||||
|
||||
openForgotPasswordModal () {
|
||||
this.forgotPasswordModal.show()
|
||||
}
|
||||
|
||||
hideForgotPasswordModal () {
|
||||
this.forgotPasswordModal.hide()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export * from './reset-password-routing.module'
|
||||
export * from './reset-password.component'
|
||||
export * from './reset-password.module'
|
|
@ -0,0 +1,25 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
|
||||
import { ResetPasswordComponent } from './reset-password.component'
|
||||
|
||||
const resetPasswordRoutes: Routes = [
|
||||
{
|
||||
path: 'reset-password',
|
||||
component: ResetPasswordComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Reset password'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [ RouterModule.forChild(resetPasswordRoutes) ],
|
||||
exports: [ RouterModule ]
|
||||
})
|
||||
export class ResetPasswordRoutingModule {}
|
|
@ -0,0 +1,33 @@
|
|||
<div class="margin-content">
|
||||
<div class="title-page title-page-single">
|
||||
Reset my password
|
||||
</div>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form role="form" (ngSubmit)="resetPassword()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password" name="password" id="password" placeholder="Password" required
|
||||
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.password" class="form-error">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-confirm">Confirm password</label>
|
||||
<input
|
||||
type="password" name="password-confirm" id="password-confirm" placeholder="Confirmed password" required
|
||||
formControlName="password-confirm" [ngClass]="{ 'input-error': formErrors['password-confirm'] }"
|
||||
>
|
||||
<div *ngIf="formErrors['password-confirm']" class="form-error">
|
||||
{{ formErrors['password-confirm'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Reset my password" [disabled]="!form.valid && isConfirmedPasswordValid()">
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
input:not([type=submit]) {
|
||||
@include peertube-input-text(340px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { USER_PASSWORD, UserService } from '@app/shared'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService } from '../core'
|
||||
import { FormReactive } from '../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-login',
|
||||
templateUrl: './reset-password.component.html',
|
||||
styleUrls: [ './reset-password.component.scss' ]
|
||||
})
|
||||
|
||||
export class ResetPasswordComponent extends FormReactive implements OnInit {
|
||||
form: FormGroup
|
||||
formErrors = {
|
||||
'password': '',
|
||||
'password-confirm': ''
|
||||
}
|
||||
validationMessages = {
|
||||
'password': USER_PASSWORD.MESSAGES,
|
||||
'password-confirm': {
|
||||
'required': 'Confirmation of the password is required.'
|
||||
}
|
||||
}
|
||||
|
||||
private userId: number
|
||||
private verificationString: string
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
buildForm () {
|
||||
this.form = this.formBuilder.group({
|
||||
password: [ '', USER_PASSWORD.VALIDATORS ],
|
||||
'password-confirm': [ '', Validators.required ]
|
||||
})
|
||||
|
||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm()
|
||||
|
||||
this.userId = this.route.snapshot.queryParams['userId']
|
||||
this.verificationString = this.route.snapshot.queryParams['verificationString']
|
||||
|
||||
if (!this.userId || !this.verificationString) {
|
||||
this.notificationsService.error('Error', 'Unable to find user id or verification string.')
|
||||
this.router.navigate([ '/' ])
|
||||
}
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
this.userService.resetPassword(this.userId, this.verificationString, this.form.value.password)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success('Success', 'Your password has been successfully reset!')
|
||||
this.router.navigate([ '/login' ])
|
||||
},
|
||||
|
||||
err => this.notificationsService.error('Error', err.message)
|
||||
)
|
||||
}
|
||||
|
||||
isConfirmedPasswordValid () {
|
||||
const values = this.form.value
|
||||
return values.password === values['password-confirm']
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { ResetPasswordRoutingModule } from './reset-password-routing.module'
|
||||
import { ResetPasswordComponent } from './reset-password.component'
|
||||
import { SharedModule } from '../shared'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ResetPasswordRoutingModule,
|
||||
SharedModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
ResetPasswordComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
ResetPasswordComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
]
|
||||
})
|
||||
export class ResetPasswordModule { }
|
|
@ -5,7 +5,6 @@ import 'rxjs/add/operator/map'
|
|||
import { UserCreate, UserUpdateMe } from '../../../../../shared'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor } from '../rest'
|
||||
import { User } from './user.model'
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
@ -54,4 +53,24 @@ export class UserService {
|
|||
return this.authHttp.get(url)
|
||||
.catch(res => this.restExtractor.handleError(res))
|
||||
}
|
||||
|
||||
askResetPassword (email: string) {
|
||||
const url = UserService.BASE_USERS_URL + '/ask-reset-password'
|
||||
|
||||
return this.authHttp.post(url, { email })
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch(res => this.restExtractor.handleError(res))
|
||||
}
|
||||
|
||||
resetPassword (userId: number, verificationString: string, password: string) {
|
||||
const url = `${UserService.BASE_USERS_URL}/${userId}/reset-password`
|
||||
const body = {
|
||||
verificationString,
|
||||
password
|
||||
}
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch(res => this.restExtractor.handleError(res))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,26 +19,30 @@
|
|||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/weak-map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
// For Google Bot
|
||||
import 'core-js/es6/symbol';
|
||||
import 'core-js/es6/object';
|
||||
import 'core-js/es6/function';
|
||||
import 'core-js/es6/parse-int';
|
||||
import 'core-js/es6/parse-float';
|
||||
import 'core-js/es6/number';
|
||||
import 'core-js/es6/math';
|
||||
import 'core-js/es6/string';
|
||||
import 'core-js/es6/date';
|
||||
import 'core-js/es6/array';
|
||||
import 'core-js/es6/regexp';
|
||||
import 'core-js/es6/map';
|
||||
import 'core-js/es6/weak-map';
|
||||
import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
// For Google Bot
|
||||
import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
|
|
|
@ -19,7 +19,7 @@ $FontPathSourceSansPro: '../../node_modules/npm-font-source-sans-pro/fonts';
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-weight: $font-regular;
|
||||
color: #000;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,15 @@ redis:
|
|||
port: 6379
|
||||
auth: null
|
||||
|
||||
smtp:
|
||||
hostname: null
|
||||
port: 465
|
||||
username: null
|
||||
password: null
|
||||
tls: true
|
||||
ca_file: null # Used for self signed certificates
|
||||
from_address: 'admin@example.com'
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'storage/avatars/'
|
||||
|
@ -37,7 +46,7 @@ cache:
|
|||
size: 1 # Max number of previews you want to cache
|
||||
|
||||
admin:
|
||||
email: 'admin@example.com'
|
||||
email: 'admin@example.com' # Your personal email as administrator
|
||||
|
||||
signup:
|
||||
enabled: false
|
||||
|
|
|
@ -20,6 +20,15 @@ redis:
|
|||
port: 6379
|
||||
auth: null
|
||||
|
||||
smtp:
|
||||
hostname: null
|
||||
port: 465
|
||||
username: null
|
||||
password: null
|
||||
tls: true
|
||||
ca_file: null # Used for self signed certificates
|
||||
from_address: 'admin@example.com'
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: '/var/www/peertube/storage/avatars/'
|
||||
|
|
|
@ -76,11 +76,13 @@
|
|||
"mkdirp": "^0.5.1",
|
||||
"morgan": "^1.5.3",
|
||||
"multer": "^1.1.0",
|
||||
"nodemailer": "^4.4.2",
|
||||
"parse-torrent": "^5.8.0",
|
||||
"password-generator": "^2.0.2",
|
||||
"pem": "^1.12.3",
|
||||
"pg": "^6.4.2",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"redis": "^2.8.0",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"request": "^2.81.0",
|
||||
"rimraf": "^2.5.4",
|
||||
|
@ -112,7 +114,9 @@
|
|||
"@types/morgan": "^1.7.32",
|
||||
"@types/multer": "^1.3.3",
|
||||
"@types/node": "^9.3.0",
|
||||
"@types/nodemailer": "^4.3.1",
|
||||
"@types/pem": "^1.9.3",
|
||||
"@types/redis": "^2.8.5",
|
||||
"@types/request": "^2.0.3",
|
||||
"@types/sequelize": "^4.0.55",
|
||||
"@types/sharp": "^0.17.6",
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
printf "############# PeerTube help #############\n\n"
|
||||
printf "npm run ...\n"
|
||||
printf " build -> Build the application for production (alias of build:client:prod)\n"
|
||||
printf " build:server:prod -> Build the server for production\n"
|
||||
printf " build:client:prod -> Build the client for production\n"
|
||||
printf " clean -> Clean the application\n"
|
||||
printf " build:server -> Build the server for production\n"
|
||||
printf " build:client -> Build the client for production\n"
|
||||
printf " clean:client -> Clean the client build files (dist directory)\n"
|
||||
printf " clean:server:test -> Clean certificates, logs, uploads and database of the test instances\n"
|
||||
printf " watch:client -> Watch the client files\n"
|
||||
printf " clean:server:test -> Clean logs, uploads, database... of the test instances\n"
|
||||
printf " watch:client -> Watch and compile on the fly the client files\n"
|
||||
printf " danger:clean:dev -> /!\ Clean certificates, logs, uploads, thumbnails, torrents and database specified in the development environment\n"
|
||||
printf " danger:clean:prod -> /!\ Clean certificates, logs, uploads, thumbnails, torrents and database specified by the production environment\n"
|
||||
printf " danger:clean:modules -> /!\ Clean node and typescript modules\n"
|
||||
|
@ -16,8 +15,7 @@ printf " play -> Run 3 fresh nodes so that you can test
|
|||
printf " reset-password -- -u [user] -> Reset the password of user [user]\n"
|
||||
printf " dev -> Watch, run the livereload and run the server so that you can develop the application\n"
|
||||
printf " start -> Run the server\n"
|
||||
printf " check -> Check the server (according to NODE_ENV)\n"
|
||||
printf " upgrade -- [branch] -> Upgrade the application according to the [branch] parameter\n"
|
||||
printf " update-host -> Upgrade scheme/host in torrent files according to the webserver configuration (config/ folder)\n"
|
||||
printf " client-report -> Open a report of the client dependencies module\n"
|
||||
printf " test -> Run the tests\n"
|
||||
printf " help -> Print this help\n"
|
||||
|
|
14
server.ts
14
server.ts
|
@ -53,9 +53,11 @@ migrate()
|
|||
|
||||
// ----------- PeerTube modules -----------
|
||||
import { installApplication } from './server/initializers'
|
||||
import { Emailer } from './server/lib/emailer'
|
||||
import { JobQueue } from './server/lib/job-queue'
|
||||
import { VideosPreviewCache } from './server/lib/cache'
|
||||
import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers'
|
||||
import { Redis } from './server/lib/redis'
|
||||
import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler'
|
||||
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
|
||||
|
||||
|
@ -169,10 +171,20 @@ function onDatabaseInitDone () {
|
|||
.then(() => {
|
||||
// ----------- Make the server listening -----------
|
||||
server.listen(port, () => {
|
||||
// Emailer initialization and then job queue initialization
|
||||
Emailer.Instance.init()
|
||||
Emailer.Instance.checkConnectionOrDie()
|
||||
.then(() => JobQueue.Instance.init())
|
||||
|
||||
// Caches initializations
|
||||
VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE)
|
||||
|
||||
// Enable Schedulers
|
||||
BadActorFollowScheduler.Instance.enable()
|
||||
RemoveOldJobsScheduler.Instance.enable()
|
||||
JobQueue.Instance.init()
|
||||
|
||||
// Redis initialization
|
||||
Redis.Instance.init()
|
||||
|
||||
logger.info('Server listening on port %d', port)
|
||||
logger.info('Web server: %s', CONFIG.WEBSERVER.URL)
|
||||
|
|
|
@ -6,17 +6,23 @@ import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRat
|
|||
import { unlinkPromise } from '../../helpers/core-utils'
|
||||
import { retryTransactionWrapper } from '../../helpers/database-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
|
||||
import { createReqFiles, generateRandomString, getFormattedObjects } from '../../helpers/utils'
|
||||
import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
|
||||
import { updateActorAvatarInstance } from '../../lib/activitypub'
|
||||
import { sendUpdateUser } from '../../lib/activitypub/send'
|
||||
import { Emailer } from '../../lib/emailer'
|
||||
import { EmailPayload } from '../../lib/job-queue/handlers/email'
|
||||
import { Redis } from '../../lib/redis'
|
||||
import { createUserAccountAndChannel } from '../../lib/user'
|
||||
import {
|
||||
asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setDefaultSort,
|
||||
setDefaultPagination, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator,
|
||||
usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator
|
||||
} from '../../middlewares'
|
||||
import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
|
||||
import {
|
||||
usersAskResetPasswordValidator, usersResetPasswordValidator, usersUpdateMyAvatarValidator,
|
||||
videosSortValidator
|
||||
} from '../../middlewares/validators'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import { UserModel } from '../../models/account/user'
|
||||
import { OAuthTokenModel } from '../../models/oauth/oauth-token'
|
||||
|
@ -106,6 +112,16 @@ usersRouter.delete('/:id',
|
|||
asyncMiddleware(removeUser)
|
||||
)
|
||||
|
||||
usersRouter.post('/ask-reset-password',
|
||||
asyncMiddleware(usersAskResetPasswordValidator),
|
||||
asyncMiddleware(askResetUserPassword)
|
||||
)
|
||||
|
||||
usersRouter.post('/:id/reset-password',
|
||||
asyncMiddleware(usersResetPasswordValidator),
|
||||
asyncMiddleware(resetUserPassword)
|
||||
)
|
||||
|
||||
usersRouter.post('/token', token, success)
|
||||
// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
|
||||
|
||||
|
@ -307,6 +323,25 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
|
|||
return res.sendStatus(204)
|
||||
}
|
||||
|
||||
async function askResetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const user = res.locals.user as UserModel
|
||||
|
||||
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)
|
||||
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
||||
async function resetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const user = res.locals.user as UserModel
|
||||
user.password = req.body.password
|
||||
|
||||
await user.save()
|
||||
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
||||
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
res.end()
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ const loggerFormat = winston.format.printf((info) => {
|
|||
if (additionalInfos === '{}') additionalInfos = ''
|
||||
else additionalInfos = ' ' + additionalInfos
|
||||
|
||||
if (info.message.stack !== undefined) info.message = info.message.stack
|
||||
return `[${info.label}] ${info.timestamp} ${info.level}: ${info.message}${additionalInfos}`
|
||||
})
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ function checkMissedConfig () {
|
|||
'webserver.https', 'webserver.hostname', 'webserver.port',
|
||||
'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password',
|
||||
'storage.videos', 'storage.logs', 'storage.thumbnails', 'storage.previews', 'storage.torrents', 'storage.cache', 'log.level',
|
||||
'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', 'user.video_quota'
|
||||
'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
|
||||
'user.video_quota', 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address'
|
||||
]
|
||||
const miss: string[] = []
|
||||
|
||||
|
|
|
@ -65,13 +65,15 @@ const JOB_ATTEMPTS: { [ id in JobType ]: number } = {
|
|||
'activitypub-http-broadcast': 5,
|
||||
'activitypub-http-unicast': 5,
|
||||
'activitypub-http-fetcher': 5,
|
||||
'video-file': 1
|
||||
'video-file': 1,
|
||||
'email': 5
|
||||
}
|
||||
const JOB_CONCURRENCY: { [ id in JobType ]: number } = {
|
||||
'activitypub-http-broadcast': 1,
|
||||
'activitypub-http-unicast': 5,
|
||||
'activitypub-http-fetcher': 1,
|
||||
'video-file': 1
|
||||
'video-file': 1,
|
||||
'email': 5
|
||||
}
|
||||
// 2 days
|
||||
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2
|
||||
|
@ -95,9 +97,18 @@ const CONFIG = {
|
|||
},
|
||||
REDIS: {
|
||||
HOSTNAME: config.get<string>('redis.hostname'),
|
||||
PORT: config.get<string>('redis.port'),
|
||||
PORT: config.get<number>('redis.port'),
|
||||
AUTH: config.get<string>('redis.auth')
|
||||
},
|
||||
SMTP: {
|
||||
HOSTNAME: config.get<string>('smtp.hostname'),
|
||||
PORT: config.get<number>('smtp.port'),
|
||||
USERNAME: config.get<string>('smtp.username'),
|
||||
PASSWORD: config.get<string>('smtp.password'),
|
||||
TLS: config.get<boolean>('smtp.tls'),
|
||||
CA_FILE: config.get<string>('smtp.ca_file'),
|
||||
FROM_ADDRESS: config.get<string>('smtp.from_address')
|
||||
},
|
||||
STORAGE: {
|
||||
AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
|
||||
LOG_DIR: buildPath(config.get<string>('storage.logs')),
|
||||
|
@ -311,6 +322,8 @@ const PRIVATE_RSA_KEY_SIZE = 2048
|
|||
// Password encryption
|
||||
const BCRYPT_SALT_SIZE = 10
|
||||
|
||||
const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Express static paths (router)
|
||||
|
@ -408,6 +421,7 @@ export {
|
|||
VIDEO_LICENCES,
|
||||
VIDEO_RATE_TYPES,
|
||||
VIDEO_MIMETYPE_EXT,
|
||||
USER_PASSWORD_RESET_LIFETIME,
|
||||
AVATAR_MIMETYPE_EXT,
|
||||
SCHEDULER_INTERVAL,
|
||||
JOB_COMPLETED_LIFETIME
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { createTransport, Transporter } from 'nodemailer'
|
||||
import { isTestInstance } from '../helpers/core-utils'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { CONFIG } from '../initializers'
|
||||
import { JobQueue } from './job-queue'
|
||||
import { EmailPayload } from './job-queue/handlers/email'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
class Emailer {
|
||||
|
||||
private static instance: Emailer
|
||||
private initialized = false
|
||||
private transporter: Transporter
|
||||
|
||||
private constructor () {}
|
||||
|
||||
init () {
|
||||
// Already initialized
|
||||
if (this.initialized === true) return
|
||||
this.initialized = true
|
||||
|
||||
if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) {
|
||||
logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
|
||||
|
||||
let tls
|
||||
if (CONFIG.SMTP.CA_FILE) {
|
||||
tls = {
|
||||
ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
|
||||
}
|
||||
}
|
||||
|
||||
this.transporter = createTransport({
|
||||
host: CONFIG.SMTP.HOSTNAME,
|
||||
port: CONFIG.SMTP.PORT,
|
||||
secure: CONFIG.SMTP.TLS,
|
||||
tls,
|
||||
auth: {
|
||||
user: CONFIG.SMTP.USERNAME,
|
||||
pass: CONFIG.SMTP.PASSWORD
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (!isTestInstance()) {
|
||||
logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkConnectionOrDie () {
|
||||
if (!this.transporter) return
|
||||
|
||||
try {
|
||||
const success = await this.transporter.verify()
|
||||
if (success !== true) this.dieOnConnectionFailure()
|
||||
|
||||
logger.info('Successfully connected to SMTP server.')
|
||||
} catch (err) {
|
||||
this.dieOnConnectionFailure(err)
|
||||
}
|
||||
}
|
||||
|
||||
addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) {
|
||||
const text = `Hi dear user,\n\n` +
|
||||
`It seems you forgot your password 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` +
|
||||
`PeerTube.`
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to: [ to ],
|
||||
subject: 'Reset your PeerTube password',
|
||||
text
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
sendMail (to: string[], subject: string, text: string) {
|
||||
if (!this.transporter) {
|
||||
throw new Error('Cannot send mail because SMTP is not configured.')
|
||||
}
|
||||
|
||||
return this.transporter.sendMail({
|
||||
from: CONFIG.SMTP.FROM_ADDRESS,
|
||||
to: to.join(','),
|
||||
subject,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
private dieOnConnectionFailure (err?: Error) {
|
||||
logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, err)
|
||||
process.exit(-1)
|
||||
}
|
||||
|
||||
static get Instance () {
|
||||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
Emailer
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import * as kue from 'kue'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { Emailer } from '../../emailer'
|
||||
|
||||
export type EmailPayload = {
|
||||
to: string[]
|
||||
subject: string
|
||||
text: string
|
||||
}
|
||||
|
||||
async function processEmail (job: kue.Job) {
|
||||
const payload = job.data as EmailPayload
|
||||
logger.info('Processing email in job %d.', job.id)
|
||||
|
||||
return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
processEmail
|
||||
}
|
|
@ -5,19 +5,22 @@ import { CONFIG, JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY } from '.
|
|||
import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast'
|
||||
import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
|
||||
import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
|
||||
import { EmailPayload, processEmail } from './handlers/email'
|
||||
import { processVideoFile, VideoFilePayload } from './handlers/video-file'
|
||||
|
||||
type CreateJobArgument =
|
||||
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
|
||||
{ type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload } |
|
||||
{ type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } |
|
||||
{ type: 'video-file', payload: VideoFilePayload }
|
||||
{ type: 'video-file', payload: VideoFilePayload } |
|
||||
{ type: 'email', payload: EmailPayload }
|
||||
|
||||
const handlers: { [ id in JobType ]: (job: kue.Job) => Promise<any>} = {
|
||||
'activitypub-http-broadcast': processActivityPubHttpBroadcast,
|
||||
'activitypub-http-unicast': processActivityPubHttpUnicast,
|
||||
'activitypub-http-fetcher': processActivityPubHttpFetcher,
|
||||
'video-file': processVideoFile
|
||||
'video-file': processVideoFile,
|
||||
'email': processEmail
|
||||
}
|
||||
|
||||
class JobQueue {
|
||||
|
@ -43,6 +46,8 @@ class JobQueue {
|
|||
}
|
||||
})
|
||||
|
||||
this.jobQueue.setMaxListeners(15)
|
||||
|
||||
this.jobQueue.on('error', err => {
|
||||
logger.error('Error in job queue.', err)
|
||||
process.exit(-1)
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { createClient, RedisClient } from 'redis'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { generateRandomString } from '../helpers/utils'
|
||||
import { CONFIG, USER_PASSWORD_RESET_LIFETIME } from '../initializers'
|
||||
|
||||
class Redis {
|
||||
|
||||
private static instance: Redis
|
||||
private initialized = false
|
||||
private client: RedisClient
|
||||
private prefix: string
|
||||
|
||||
private constructor () {}
|
||||
|
||||
init () {
|
||||
// Already initialized
|
||||
if (this.initialized === true) return
|
||||
this.initialized = true
|
||||
|
||||
this.client = createClient({
|
||||
host: CONFIG.REDIS.HOSTNAME,
|
||||
port: CONFIG.REDIS.PORT
|
||||
})
|
||||
|
||||
this.client.on('error', err => {
|
||||
logger.error('Error in Redis client.', err)
|
||||
process.exit(-1)
|
||||
})
|
||||
|
||||
if (CONFIG.REDIS.AUTH) {
|
||||
this.client.auth(CONFIG.REDIS.AUTH)
|
||||
}
|
||||
|
||||
this.prefix = 'redis-' + CONFIG.WEBSERVER.HOST + '-'
|
||||
}
|
||||
|
||||
async setResetPasswordVerificationString (userId: number) {
|
||||
const generatedString = await generateRandomString(32)
|
||||
|
||||
await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
|
||||
|
||||
return generatedString
|
||||
}
|
||||
|
||||
async getResetPasswordLink (userId: number) {
|
||||
return this.getValue(this.generateResetPasswordKey(userId))
|
||||
}
|
||||
|
||||
private getValue (key: string) {
|
||||
return new Promise<string>((res, rej) => {
|
||||
this.client.get(this.prefix + key, (err, value) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
return res(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private setValue (key: string, value: string, expirationMilliseconds: number) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
if (ok !== 'OK') return rej(new Error('Redis result is not OK.'))
|
||||
|
||||
return res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private generateResetPasswordKey (userId: number) {
|
||||
return 'reset-password-' + userId
|
||||
}
|
||||
|
||||
static get Instance () {
|
||||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
Redis
|
||||
}
|
|
@ -1,18 +1,25 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import * as express from 'express'
|
||||
import 'express-validator'
|
||||
import { body, param } from 'express-validator/check'
|
||||
import { omit } from 'lodash'
|
||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
||||
import {
|
||||
isAvatarFile, isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
|
||||
isAvatarFile,
|
||||
isUserAutoPlayVideoValid,
|
||||
isUserDisplayNSFWValid,
|
||||
isUserPasswordValid,
|
||||
isUserRoleValid,
|
||||
isUserUsernameValid,
|
||||
isUserVideoQuotaValid
|
||||
} from '../../helpers/custom-validators/users'
|
||||
import { isVideoExist } from '../../helpers/custom-validators/videos'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { isSignupAllowed } from '../../helpers/utils'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { Redis } from '../../lib/redis'
|
||||
import { UserModel } from '../../models/account/user'
|
||||
import { areValidationErrors } from './utils'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
const usersAddValidator = [
|
||||
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
|
||||
|
@ -167,6 +174,49 @@ const ensureUserRegistrationAllowed = [
|
|||
}
|
||||
]
|
||||
|
||||
const usersAskResetPasswordValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
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)
|
||||
// Do not leak our emails
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const usersResetPasswordValidator = [
|
||||
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
|
||||
body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user as UserModel
|
||||
const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ error: 'Invalid verification string.' })
|
||||
.end
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -178,24 +228,19 @@ export {
|
|||
usersVideoRatingValidator,
|
||||
ensureUserRegistrationAllowed,
|
||||
usersGetValidator,
|
||||
usersUpdateMyAvatarValidator
|
||||
usersUpdateMyAvatarValidator,
|
||||
usersAskResetPasswordValidator,
|
||||
usersResetPasswordValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserIdExist (id: number, res: express.Response) {
|
||||
const user = await UserModel.loadById(id)
|
||||
function checkUserIdExist (id: number, res: express.Response) {
|
||||
return checkUserExist(() => UserModel.loadById(id), res)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
res.status(404)
|
||||
.send({ error: 'User not found' })
|
||||
.end()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
|
||||
}
|
||||
|
||||
async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
|
||||
|
@ -210,3 +255,21 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
async function checkUserExist (finder: () => Bluebird<UserModel>, res: express.Response, abortResponse = true) {
|
||||
const user = await finder()
|
||||
|
||||
if (!user) {
|
||||
if (abortResponse === true) {
|
||||
res.status(404)
|
||||
.send({ error: 'User not found' })
|
||||
.end()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -161,6 +161,16 @@ export class UserModel extends Model<UserModel> {
|
|||
return UserModel.scope('withVideoChannel').findOne(query)
|
||||
}
|
||||
|
||||
static loadByEmail (email: string) {
|
||||
const query = {
|
||||
where: {
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
return UserModel.findOne(query)
|
||||
}
|
||||
|
||||
static loadByUsernameOrEmail (username: string, email?: string) {
|
||||
if (!email) email = username
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ export type JobState = 'active' | 'complete' | 'failed' | 'inactive' | 'delayed'
|
|||
export type JobType = 'activitypub-http-unicast' |
|
||||
'activitypub-http-broadcast' |
|
||||
'activitypub-http-fetcher' |
|
||||
'video-file'
|
||||
'video-file' |
|
||||
'email'
|
||||
|
||||
export interface Job {
|
||||
id: number
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"storage",
|
||||
"client",
|
||||
"test1",
|
||||
"test2",
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -134,6 +134,12 @@
|
|||
version "6.0.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.41.tgz#578cf53aaec65887bcaf16792f8722932e8ff8ea"
|
||||
|
||||
"@types/nodemailer@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-4.3.1.tgz#e3985c1b7c7bbbb2a886108b89f1c7ce9a690654"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/parse-torrent-file@*":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.1.tgz#056a6c18f3fac0cd7c6c74540f00496a3225976b"
|
||||
|
@ -152,7 +158,7 @@
|
|||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.3.tgz#0c864c8b79e43fef6367db895f60fd1edd10e86c"
|
||||
|
||||
"@types/redis@*":
|
||||
"@types/redis@*", "@types/redis@^2.8.5":
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.5.tgz#c4a31a63e95434202eb84908290528ad8510b149"
|
||||
dependencies:
|
||||
|
@ -4274,6 +4280,10 @@ node-sass@^4.0.0:
|
|||
stdout-stream "^1.4.0"
|
||||
"true-case-path" "^1.0.2"
|
||||
|
||||
nodemailer@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.4.2.tgz#f215fb88e8a1052f9f93083909e116d2b79fc8de"
|
||||
|
||||
nodemon@^1.11.0:
|
||||
version "1.14.11"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.14.11.tgz#cc0009dd8d82f126f3aba50ace7e753827a8cebc"
|
||||
|
@ -5149,7 +5159,7 @@ redis-commands@^1.2.0:
|
|||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b"
|
||||
|
||||
redis-parser@^2.0.0:
|
||||
redis-parser@^2.0.0, redis-parser@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"
|
||||
|
||||
|
@ -5157,6 +5167,14 @@ redis@^0.12.1:
|
|||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e"
|
||||
|
||||
redis@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02"
|
||||
dependencies:
|
||||
double-ended-queue "^2.1.0-0"
|
||||
redis-commands "^1.2.0"
|
||||
redis-parser "^2.6.0"
|
||||
|
||||
redis@~2.6.0-2:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687"
|
||||
|
|
Loading…
Reference in New Issue