Waiting for token reset

This commit is contained in:
Fergal Moran
2018-05-06 23:23:23 +01:00
parent c0bf822fe0
commit fd45069103
16 changed files with 190 additions and 57 deletions

View File

@@ -16,7 +16,7 @@ import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
import { AngularFireDatabaseModule } from 'angularfire2/database'; import { AngularFireDatabaseModule } from 'angularfire2/database';
import { AngularFireAuthModule } from 'angularfire2/auth'; import { AngularFireAuthModule } from 'angularfire2/auth';
import { AngularFireModule } from 'angularfire2'; import { AngularFireModule } from 'angularfire2';
import { QuillModule } from 'ngx-quill' import { QuillModule } from 'ngx-quill';
import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login'; import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login';
import { import {

View File

@@ -1,6 +1,7 @@
import { Component, OnInit, ReflectiveInjector } from '@angular/core'; import { Component, OnInit, ReflectiveInjector } from '@angular/core';
import { AppInsightsService } from '../../services/app-insights.service'; import { AppInsightsService } from '../../services/app-insights.service';
import { environment } from 'environments/environment';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'app-base-page', selector: 'app-base-page',
template: '' template: ''
@@ -18,4 +19,10 @@ export class BasePageComponent {
private logNavigation() { private logNavigation() {
this._appInsightsService.logPageView(); this._appInsightsService.logPageView();
} }
protected formatError(error: string): string {
return `${error}<br />Please visit <a href="${
environment.HELP_URL
}">here</a> and request help.`;
}
} }

View File

@@ -49,8 +49,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-danger" *ngIf="errorMessage" role="alert"> <div class="alert alert-danger" *ngIf="errorMessage" role="alert" [innerHTML]="errorMessage">
{{errorMessage}}
</div> </div>
<div class="col-md-12 new-user-alert" *ngIf="brandNew"> <div class="col-md-12 new-user-alert" *ngIf="brandNew">
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">

View File

@@ -39,8 +39,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-danger" *ngIf="errorMessage" role="alert"> <div class="alert alert-danger" *ngIf="errorMessage" [innerHTML]="errorMessage">
{{errorMessage}}
</div> </div>
<div class="form-group row gutters-tiny"> <div class="form-group row gutters-tiny">
<div class="col-12 mb-10"> <div class="col-12 mb-10">

View File

@@ -1,8 +1,6 @@
<div id="page-container" <div id="page-container" class="main-content-boxed">
class="main-content-boxed">
<main id="main-container"> <main id="main-container">
<div class="bg-image" <div class="bg-image" style="background-image: url('/assets/img/robothand.jpg'); background-size: 100% 100%;">
style="background-image: url('/assets/img/robothand.jpg'); background-size: 100% 100%;">
<div class="hero-static content content-full bg-white"> <div class="hero-static content content-full bg-white">
<div class="text-center"> <div class="text-center">
<a class="link-effect font-w700"> <a class="link-effect font-w700">
@@ -14,44 +12,47 @@
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-sm-8 col-md-6 col-xl-4"> <div class="col-sm-8 col-md-6 col-xl-4">
<form class="js-validation-signin" <form class="js-validation-signin" action="" (ngSubmit)="resetPassword()" method="post">
action="" <div class="form-group row" *ngIf="noToken">
(ngSubmit)="resetPassword()"
method="post">
<div class="form-group row">
<div class="col-12"> <div class="col-12">
<div class="form-material floating"> <div class="form-material floating">
<input type="text" <input type="text" class="form-control" id="login-username" name="login-username" [(ngModel)]="username">
class="form-control"
id="login-username"
name="login-username"
[(ngModel)]="username">
<label for="login-username">Email Address</label> <label for="login-username">Email Address</label>
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-danger" <div class="form-group row" *ngIf="!noToken">
*ngIf="errorMessage" <div class="col-12">
role="alert"> <div class="form-material ">
{{errorMessage}} <input type="password" class="form-control" id="login-password" name="login-password" [(ngModel)]="newPassword">
<label for="login-password">New password</label>
</div> </div>
<div class="form-group row gutters-tiny" </div>
*ngIf="successMessage"> </div>
<div class="form-group row" *ngIf="!noToken">
<div class="col-12 ">
<div class="form-material ">
<input type="password " class="form-control " id="login-password2 " name="login-password2
" [(ngModel)]="newPasswordRepeat ">
<label for="login-password ">Repeat password</label>
</div>
</div>
</div>
<div class="alert alert-danger " *ngIf="errorMessage " role="alert " [innerHTML]="errorMessage ">
</div>
<div class="form-group row gutters-tiny " *ngIf="successMessage ">
<div class="col-12 mb-10 "> <div class="col-12 mb-10 ">
<div class="alert alert-success" <div class="alert alert-success " role="alert ">
role="alert">
<strong>Success!</strong> {{successMessage}} <strong>Success!</strong> {{successMessage}}
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row gutters-tiny "> <div class="form-group row gutters-tiny ">
<div class="col-12 mb-10 "> <div class="col-12 mb-10 ">
<a [routerLink]="['']" <a [routerLink]="[ ''] " *ngIf="successMessage " class="btn btn-block btn-hero btn-noborder
*ngIf="successMessage" btn-rounded btn-alt-secondary ">Go Home..</a>
class="btn btn-block btn-hero btn-noborder btn-rounded btn-alt-secondary">Go Home..</a> <button type="submit " *ngIf="!successMessage " class="btn btn-block btn-hero btn-noborder
<button type="submit" btn-rounded btn-alt-primary ">
*ngIf="!successMessage"
class="btn btn-block btn-hero btn-noborder btn-rounded btn-alt-primary">
<i class="si si-reload mr-10 "></i> Reset Password <i class="si si-reload mr-10 "></i> Reset Password
</button> </button>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { BasePageComponent } from '../base-page/base-page.component'; import { BasePageComponent } from '../base-page/base-page.component';
import { AppInsightsService } from '../../services/app-insights.service';
@Component({ @Component({
selector: 'app-reset', selector: 'app-reset',
@@ -13,23 +14,41 @@ export class ResetComponent extends BasePageComponent implements OnInit {
username: string; username: string;
errorMessage: string; errorMessage: string;
successMessage: string; successMessage: string;
constructor(private _authService: PodnomsAuthService) { noToken: boolean = true;
newPassword: string;
newPasswordRepeat: string;
constructor(
private _authService: PodnomsAuthService,
private _insightsService: AppInsightsService
) {
super(); super();
} }
ngOnInit() {} ngOnInit() {}
resetPassword() { resetPassword() {
if (this.username) { if (this.username) {
this._authService.resetPassword(this.username); this._authService.resetPassword(this.username).subscribe(
// .catch(err => { (result) => {
// this.errorMessage = err.description; if (result.status === 200) {
// return Observable.of(`Error resetting password: ${err.description}`); console.log('reset.component.ts', 'method', result);
// }) this.errorMessage = '';
// .subscribe(result => { this.successMessage = `A password reset link has been sent to ${
// console.log('reset.component.ts', 'method', result); this.username
// this.errorMessage = ''; }`;
// this.successMessage = `A password reset link has been sent to ${this.username}`; } else {
// }); this.errorMessage =
'Unable to reset your password\nPlease visit https://talk.podnoms.com and request help.';
}
},
(err) => {
this.errorMessage = this.formatError(
'Unable to reset your password'
);
this._insightsService.logEvent('client_error', {
message: err.message
});
}
);
} else { } else {
this.errorMessage = 'Please enter your email address'; this.errorMessage = 'Please enter your email address';
} }

View File

@@ -127,6 +127,15 @@ export class PodnomsAuthService extends BaseService {
window.location.reload(true); window.location.reload(true);
}, 0); }, 0);
} }
public resetPassword(userName: string) {} public resetPassword(userName: string): Observable<Response> {
public loginSocial(provider: string): void {} const body = JSON.stringify({
email: userName
});
return this._http
.post<Response>(
environment.API_HOST + '/auth/forgotpassword',
body,
this.httpOptions
);
}
} }

View File

@@ -4,7 +4,11 @@ export const environment = {
API_HOST: 'https://api.podnoms.com', API_HOST: 'https://api.podnoms.com',
SIGNALR_HOST: 'https://rt.podnoms.com', SIGNALR_HOST: 'https://rt.podnoms.com',
BASE_URL: 'https://podnoms.com', BASE_URL: 'https://podnoms.com',
HELP_URL: 'https://talk.podnoms.com',
appInsights: { appInsights: {
instrumentationKey: '020b002a-bd3d-4b25-8a74-cab16fd39dfc' instrumentationKey: '020b002a-bd3d-4b25-8a74-cab16fd39dfc'
},
messaging: {
endpoint: 'https://fcm.googleapis.com/fcm/send'
} }
}; };

View File

@@ -7,6 +7,7 @@ export const environment = {
DOMAIN: 'localhost', DOMAIN: 'localhost',
SIGNALR_HOST: ROOT_URL, SIGNALR_HOST: ROOT_URL,
BASE_URL: 'http://localhost:4200/', BASE_URL: 'http://localhost:4200/',
HELP_URL: 'https://talk.podnoms.com',
appInsights: { appInsights: {
instrumentationKey: '020b002a-bd3d-4b25-8a74-cab16fd39dfc' instrumentationKey: '020b002a-bd3d-4b25-8a74-cab16fd39dfc'
}, },

View File

@@ -1,6 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -14,11 +16,14 @@ namespace PodNoms.Api.Controllers {
public class AuthController : Controller { public class AuthController : Controller {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly IJwtFactory _jwtFactory; private readonly IJwtFactory _jwtFactory;
private readonly IEmailSender _emailSender;
private readonly JwtIssuerOptions _jwtOptions; private readonly JwtIssuerOptions _jwtOptions;
public AuthController(UserManager<ApplicationUser> userManager, IJwtFactory jwtFactory, IOptions<JwtIssuerOptions> jwtOptions) { public AuthController(UserManager<ApplicationUser> userManager, IJwtFactory jwtFactory, IOptions<JwtIssuerOptions> jwtOptions,
IEmailSender emailSender) {
_userManager = userManager; _userManager = userManager;
_jwtFactory = jwtFactory; _jwtFactory = jwtFactory;
_emailSender = emailSender;
_jwtOptions = jwtOptions.Value; _jwtOptions = jwtOptions.Value;
} }
@@ -58,5 +63,44 @@ namespace PodNoms.Api.Controllers {
// Credentials are invalid, or account doesn't exist // Credentials are invalid, or account doesn't exist
return await Task.FromResult<ClaimsIdentity>(null); return await Task.FromResult<ClaimsIdentity>(null);
} }
[HttpPost("reset")]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword([FromBody]ForgotPasswordViewModel model) {
if (ModelState.IsValid) {
var user = await _userManager.FindByNameAsync(model.Email);
if (user == null) {
// Don't reveal that the user does not exist or is not confirmed
return BadRequest(model);
}
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
"Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");
return Ok(model);
}
// If we got this far, something failed, redisplay form
return BadRequest(model);
}
[HttpPost("/reset")]
[AllowAnonymous]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model) {
if (!ModelState.IsValid) {
return BadRequest("Unable to reset your password at this time");
}
var user = await _userManager.FindByNameAsync(model.Email);
if (user == null) {
return BadRequest("Unable to reset your password at this time");
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded) {
return BadRequest();
}
return Ok();
}
} }
} }

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace PodNoms.Api.Models.ViewModels {
public class ForgotPasswordViewModel {
[Required]
[EmailAddress]
public string Email { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace PodNoms.Api.Models.ViewModels {
public class ResetPasswordViewModel {
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string Code { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.UI.Services;
using PodNoms.Api.Services;
namespace PodNoms.Api.Services.Auth {
public class EmailSender : IEmailSender {
private readonly IMailSender _mailSender;
public EmailSender(IMailSender mailSender) {
this._mailSender = mailSender;
}
public async Task SendEmailAsync(string email, string subject, string htmlMessage) {
await this._mailSender.SendEmail(email, subject, htmlMessage);
}
}
}

View File

@@ -45,6 +45,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Identity.UI.Services;
namespace PodNoms.Api { namespace PodNoms.Api {
public class Startup { public class Startup {
@@ -218,6 +219,7 @@ namespace PodNoms.Api {
services.AddScoped<IUrlProcessService, UrlProcessService>(); services.AddScoped<IUrlProcessService, UrlProcessService>();
services.AddScoped<INotifyJobCompleteService, NotifyJobCompleteService>(); services.AddScoped<INotifyJobCompleteService, NotifyJobCompleteService>();
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>(); services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
services.AddScoped<IEmailSender, PodNoms.Api.Services.Auth.EmailSender>();
services.AddScoped<IMailSender, MailgunSender>(); services.AddScoped<IMailSender, MailgunSender>();
services.AddHttpClient<Services.Gravatar.GravatarHttpClient>(); services.AddHttpClient<Services.Gravatar.GravatarHttpClient>();

View File

@@ -9,6 +9,7 @@
}, },
"App": { "App": {
"Version": "0.22.0", "Version": "0.22.0",
"SiteUrl": "http://localhost:4200",
"RssUrl": "http://localhost:5000/rss/" "RssUrl": "http://localhost:5000/rss/"
}, },
"ConnectionStrings": { "ConnectionStrings": {