#1456 Reset Password stuff #865

pull/1488/head
Jamie.Rees 7 years ago
parent 51fbd56c44
commit ace90da7ed

@ -90,6 +90,7 @@ namespace Ombi.DependencyInjection
{
services.AddTransient<IRequestServiceMain, RequestService>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>();

@ -16,10 +16,11 @@ namespace Ombi.Notifications.Agents
{
public class EmailNotification : BaseNotification<EmailNotificationSettings>, IEmailNotification
{
public EmailNotification(ISettingsService<EmailNotificationSettings> settings, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t) : base(settings, r, m, t)
public EmailNotification(ISettingsService<EmailNotificationSettings> settings, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, IEmailProvider prov) : base(settings, r, m, t)
{
EmailProvider = prov;
}
private IEmailProvider EmailProvider { get; }
public override string NotificationName => nameof(EmailNotification);
protected override bool ValidateConfiguration(EmailNotificationSettings settings)
@ -166,44 +167,7 @@ namespace Ombi.Notifications.Agents
protected override async Task Send(NotificationMessage model, EmailNotificationSettings settings)
{
try
{
var body = new BodyBuilder
{
HtmlBody = model.Message,
//TextBody = model.Other["PlainTextBody"]
};
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = model.Subject
};
message.From.Add(new MailboxAddress(settings.Sender, settings.Sender));
message.To.Add(new MailboxAddress(model.To, model.To));
using (var client = new SmtpClient())
{
client.Connect(settings.Host, settings.Port); // Let MailKit figure out the correct SecureSocketOptions.
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
client.AuthenticationMechanisms.Remove("XOAUTH2");
if (settings.Authentication)
{
client.Authenticate(settings.Username, settings.Password);
}
//Log.Info("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
catch (Exception e)
{
//Log.Error(e);
throw new InvalidOperationException(e.Message);
}
await EmailProvider.Send(model, settings);
}
protected override async Task Test(NotificationOptions model, EmailNotificationSettings settings)

@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using MimeKit;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models.Notifications;
namespace Ombi.Notifications
{
public class GenericEmailProvider : IEmailProvider
{
public async Task Send(NotificationMessage model, EmailNotificationSettings settings)
{
try
{
var body = new BodyBuilder
{
HtmlBody = model.Message,
//TextBody = model.Other["PlainTextBody"]
};
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = model.Subject
};
message.From.Add(new MailboxAddress(settings.Sender, settings.Sender));
message.To.Add(new MailboxAddress(model.To, model.To));
using (var client = new SmtpClient())
{
client.Connect(settings.Host, settings.Port); // Let MailKit figure out the correct SecureSocketOptions.
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
client.AuthenticationMechanisms.Remove("XOAUTH2");
if (settings.Authentication)
{
client.Authenticate(settings.Username, settings.Password);
}
//Log.Info("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
catch (Exception e)
{
//Log.Error(e);
throw new InvalidOperationException(e.Message);
}
}
}
}

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models.Notifications;
namespace Ombi.Notifications
{
public interface IEmailProvider
{
Task Send(NotificationMessage model, EmailNotificationSettings settings);
}
}

@ -7,7 +7,7 @@ import { RouterModule, Routes } from '@angular/router';
import { HttpModule } from '@angular/http';
// Third Party
import { ButtonModule, DialogModule } from 'primeng/primeng';
import { ButtonModule, DialogModule, CaptchaModule } from 'primeng/primeng';
import { GrowlModule } from 'primeng/components/growl/growl';
import { DataTableModule, SharedModule } from 'primeng/primeng';
//import { DragulaModule, DragulaService } from 'ng2-dragula/ng2-dragula';
@ -17,6 +17,8 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { ResetPasswordComponent } from './login/resetpassword.component';
import { TokenResetPasswordComponent } from './login/tokenresetpassword.component';
import { LandingPageComponent } from './landingpage/landingpage.component';
import { PageNotFoundComponent } from './errors/not-found.component';
@ -44,6 +46,7 @@ const routes: Routes = [
//{ path: 'requests-grid', component: RequestGridComponent },
{ path: 'login', component: LoginComponent },
{ path: 'reset', component: ResetPasswordComponent },
{ path: 'landingpage', component: LandingPageComponent }
];
@ -71,13 +74,16 @@ const routes: Routes = [
MdTabsModule,
ReactiveFormsModule,
UserManagementModule,
RequestsModule
RequestsModule,
CaptchaModule
],
declarations: [
AppComponent,
PageNotFoundComponent,
LoginComponent,
LandingPageComponent,
ResetPasswordComponent,
TokenResetPasswordComponent
],
providers: [
RequestService,

@ -30,3 +30,8 @@ export interface IUpdateLocalUser extends IUser {
confirmNewPassword: string
}
export interface IResetPasswordToken{
email:string,
token:string,
password:string
}

@ -17,8 +17,8 @@ include the remember me checkbox
<input type="password" id="inputPassword" class="form-control" formControlName="password" placeholder="Password">
<button class="btn btn-success-outline" [disabled]="form.invalid" type="submit">Sign in</button>
</form><!-- /form -->
<a href="#" class="forgot-password">
Forgot the password?
<a [routerLink]="['/reset']" class="forgot-password">
Reset your password?
</a>
</div><!-- /card-container -->
</div><!-- /container -->

@ -0,0 +1,20 @@
<!--
you can substitue the span of reauth email for a input with the email and
include the remember me checkbox
-->
<div *ngIf="form && customizationSettings">
<div class="container" id="login">
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="/images/ms-icon-150x150.png" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="profile-img-card" [src]="customizationSettings.logo" /></div>
<p id="profile-name" class="profile-name-card"></p>
<form class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<input type="email" id="inputEmail" class="form-control" formControlName="email" placeholder="Email Address" autofocus>
<button class="btn btn-success-outline" [disabled]="form.invalid" type="submit">Reset Password</button>
</form><!-- /form -->
</div><!-- /card-container -->
</div><!-- /container -->
</div>

@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { IdentityService } from '../services/identity.service';
import { NotificationService } from '../services/notification.service';
import { SettingsService } from '../services/settings.service';
import { ICustomizationSettings } from '../interfaces/ISettings';
@Component({
templateUrl: './resetpassword.component.html',
styleUrls: ['./login.component.scss']
})
export class ResetPasswordComponent implements OnInit {
constructor(private identityService: IdentityService, private notify: NotificationService,
private fb: FormBuilder, private settingsService: SettingsService) {
this.form = this.fb.group({
email: ["", [Validators.required]],
});
}
form: FormGroup;
customizationSettings: ICustomizationSettings;
ngOnInit(): void {
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
}
onSubmit(form: FormGroup): void {
if (form.invalid) {
this.notify.error("Validation", "Email address is required");
return
}
this.identityService.submitResetPassword(form.value.email).subscribe(x => {
x.errors.forEach((val) => {
this.notify.success("Password Reset", val);
});
});
}
}

@ -0,0 +1,37 @@
<!--
you can substitue the span of reauth email for a input with the email and
include the remember me checkbox
-->
<div *ngIf="form && customizationSettings">
<div class="container" id="login">
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="/images/ms-icon-150x150.png" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="profile-img-card" [src]="customizationSettings.logo" /></div>
<p id="profile-name" class="profile-name-card"></p>
<div *ngIf="form.value.password !== form.value.confirmPassword" class="alert alert-danger">The passwords do not match</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('password').hasError('required')">The Password is required</div>
<div *ngIf="form.get('email').hasError('required')">The Email is required</div>
<div *ngIf="form.get('confirmPassword').hasError('required')">The Confirm Password is required</div>
</div>
<form class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<input type="email" id="inputEmail" class="form-control" formControlName="email" placeholder="Email Address" autofocus>
<input type="password" class="form-control" formControlName="password">
<input type="password" class="form-control" formControlName="confirmPassword">
<button class="btn btn-success-outline" [disabled]="form.invalid || !captcha" type="submit">Reset Password</button>
</form>
<!-- /form -->
<a href="#" class="forgot-password">
Forgot the password?
</a>
</div>
<!-- /card-container -->
</div>
<!-- /container -->
</div>

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { IdentityService } from '../services/identity.service';
import { NotificationService } from '../services/notification.service';
import { SettingsService } from '../services/settings.service';
import { ICustomizationSettings } from '../interfaces/ISettings';
@Component({
templateUrl: './tokenresetpassword.component.html',
styleUrls: ['./login.component.scss']
})
export class TokenResetPasswordComponent implements OnInit {
constructor(private identityService: IdentityService, private router: Router, private route: ActivatedRoute, private notify: NotificationService,
private fb: FormBuilder, private settingsService: SettingsService) {
this.route.params
.subscribe(params => {
this.form = this.fb.group({
email: ["", [Validators.required]],
password: ["", [Validators.required]],
confirmPassword: ["", [Validators.required]],
token: [params['token']]
});
});
}
form: FormGroup;
customizationSettings: ICustomizationSettings;
ngOnInit(): void {
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
}
onSubmit(form: FormGroup): void {
if (form.invalid) {
this.notify.error("Validation", "Email address is required");
return
}
this.identityService.resetPassword(form.value).subscribe(x => {
if (x.successful) {
this.notify.success("Success", `Your Password has been reset`)
this.router.navigate(['login']);
} else {
x.errors.forEach((val) => {
this.notify.error("Error", val);
});
}
});
}
}

@ -4,7 +4,7 @@ import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { ServiceAuthHelpers } from './service.helpers';
import { IUser, IUpdateLocalUser, ICheckbox, IIdentityResult } from '../interfaces/IUser';
import { IUser, IUpdateLocalUser, ICheckbox, IIdentityResult, IResetPasswordToken } from '../interfaces/IUser';
@Injectable()
@ -47,6 +47,14 @@ export class IdentityService extends ServiceAuthHelpers {
return this.http.delete(`${this.url}/${user.id}`, { headers: this.headers }).map(this.extractData);
}
submitResetPassword(email:string): Observable<IIdentityResult>{
return this.regularHttp.post(this.url + 'reset', JSON.stringify({email:email}), { headers: this.headers }).map(this.extractData);
}
resetPassword(token: IResetPasswordToken):Observable<IIdentityResult>{
return this.regularHttp.post(this.url + 'resetpassword', JSON.stringify(token), { headers: this.headers }).map(this.extractData);
}
hasRole(role: string): boolean {
var roles = localStorage.getItem("roles") as string[] | null;
if (roles) {

@ -9,15 +9,22 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Ombi.Attributes;
using Ombi.Config;
using Ombi.Core.Claims;
using Ombi.Core.Helpers;
using Ombi.Core.Models.UI;
using Ombi.Core.Settings;
using Ombi.Models;
using Ombi.Models.Identity;
using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using IdentityResult = Ombi.Models.Identity.IdentityResult;
using OmbiIdentityResult = Ombi.Models.Identity.IdentityResult;
namespace Ombi.Controllers
{
@ -28,16 +35,27 @@ namespace Ombi.Controllers
[PowerUser]
public class IdentityController : BaseV1ApiController
{
public IdentityController(UserManager<OmbiUser> user, IMapper mapper, RoleManager<IdentityRole> rm)
public IdentityController(UserManager<OmbiUser> user, IMapper mapper, RoleManager<IdentityRole> rm, IEmailProvider prov,
ISettingsService<EmailNotificationSettings> s,
ISettingsService<CustomizationSettings> c,
IOptions<UserSettings> userSettings)
{
UserManager = user;
Mapper = mapper;
RoleManager = rm;
EmailProvider = prov;
EmailSettings = s;
CustomizationSettings = c;
UserSettings = userSettings;
}
private UserManager<OmbiUser> UserManager { get; }
private RoleManager<IdentityRole> RoleManager { get; }
private IMapper Mapper { get; }
private IEmailProvider EmailProvider { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
private IOptions<UserSettings> UserSettings { get; }
/// <summary>
/// This is what the Wizard will call when creating the user for the very first time.
@ -167,7 +185,7 @@ namespace Ombi.Controllers
/// <param name = "user" > The user.</param>
/// <returns></returns>
[HttpPost]
public async Task<IdentityResult> CreateUser([FromBody] UserViewModel user)
public async Task<OmbiIdentityResult> CreateUser([FromBody] UserViewModel user)
{
if (!EmailValidator.IsValidEmail(user.EmailAddress))
{
@ -185,7 +203,7 @@ namespace Ombi.Controllers
if (!userResult.Succeeded)
{
// We did not create the user
return new IdentityResult
return new OmbiIdentityResult
{
Errors = userResult.Errors.Select(x => x.Description).ToList()
};
@ -201,13 +219,13 @@ namespace Ombi.Controllers
messages.AddRange(errors.Errors.Select(x => x.Description).ToList());
}
return new IdentityResult
return new OmbiIdentityResult
{
Errors = messages
};
}
return new IdentityResult
return new OmbiIdentityResult
{
Successful = true
};
@ -220,7 +238,7 @@ namespace Ombi.Controllers
/// <returns></returns>
[HttpPut("local")]
[Authorize]
public async Task<IdentityResult> UpdateLocalUser([FromBody] UpdateLocalUserModel ui)
public async Task<OmbiIdentityResult> UpdateLocalUser([FromBody] UpdateLocalUserModel ui)
{
if (string.IsNullOrEmpty(ui.CurrentPassword))
{
@ -260,7 +278,7 @@ namespace Ombi.Controllers
var updateResult = await UserManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
return new IdentityResult
return new OmbiIdentityResult
{
Errors = updateResult.Errors.Select(x => x.Description).ToList()
};
@ -272,13 +290,13 @@ namespace Ombi.Controllers
if (!result.Succeeded)
{
return new IdentityResult
return new OmbiIdentityResult
{
Errors = result.Errors.Select(x => x.Description).ToList()
};
}
}
return new IdentityResult
return new OmbiIdentityResult
{
Successful = true
};
@ -291,7 +309,7 @@ namespace Ombi.Controllers
/// <param name = "ui" > The user.</param>
/// <returns></returns>
[HttpPut]
public async Task<IdentityResult> UpdateUser([FromBody] UserViewModel ui)
public async Task<OmbiIdentityResult> UpdateUser([FromBody] UserViewModel ui)
{
if (!EmailValidator.IsValidEmail(ui.EmailAddress))
{
@ -304,7 +322,7 @@ namespace Ombi.Controllers
var updateResult = await UserManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
return new IdentityResult
return new OmbiIdentityResult
{
Errors = updateResult.Errors.Select(x => x.Description).ToList()
};
@ -327,13 +345,13 @@ namespace Ombi.Controllers
messages.AddRange(errors.Errors.Select(x => x.Description).ToList());
}
return new IdentityResult
return new OmbiIdentityResult
{
Errors = messages
};
}
return new IdentityResult
return new OmbiIdentityResult
{
Successful = true
};
@ -346,7 +364,7 @@ namespace Ombi.Controllers
/// <param name="userId">The user.</param>
/// <returns></returns>
[HttpDelete("{userId}")]
public async Task<IdentityResult> DeleteUser(string userId)
public async Task<OmbiIdentityResult> DeleteUser(string userId)
{
var userToDelete = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == userId);
@ -355,13 +373,13 @@ namespace Ombi.Controllers
var result = await UserManager.DeleteAsync(userToDelete);
if (result.Succeeded)
{
return new IdentityResult
return new OmbiIdentityResult
{
Successful = true
};
}
return new IdentityResult
return new OmbiIdentityResult
{
Errors = result.Errors.Select(x => x.Description).ToList()
};
@ -391,6 +409,86 @@ namespace Ombi.Controllers
return claims;
}
/// <summary>
/// Send out the email with the reset link
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
[HttpPost("reset")]
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody]SubmitPasswordReset email)
{
// Check if account exists
var user = await UserManager.FindByEmailAsync(email.Email);
var defaultMessage = new OmbiIdentityResult
{
Successful = true,
Errors = new List<string> { "If this account exists you should recieve a password reset link." }
};
if (user == null)
{
return defaultMessage;
}
// We have the user
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
// We now need to email the user with this token
var emailSettings = await EmailSettings.GetSettingsAsync();
var customizationSettings = await CustomizationSettings.GetSettingsAsync();
var appName = (string.IsNullOrEmpty(customizationSettings.ApplicationName)
? "Ombi"
: customizationSettings.ApplicationName);
await EmailProvider.Send(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message = $"Hello {user.UserName}, <br/> You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"{UserSettings.Value.WebsiteUrl}/reset/{token}\"> Reset </a>"
}, emailSettings);
return defaultMessage;
}
/// <summary>
/// Resets the password
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("resetpassword")]
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<OmbiIdentityResult> ResetPassword(ResetPasswordToken token)
{
var user = await UserManager.FindByEmailAsync(token.Email);
if (user == null)
{
return new OmbiIdentityResult
{
Successful = false,
Errors = new List<string> { "Please check you email." }
};
}
var tokenValid = await UserManager.ResetPasswordAsync(user, token.Token, token.Password);
if (tokenValid.Succeeded)
{
return new OmbiIdentityResult
{
Successful = true,
};
}
return new OmbiIdentityResult
{
Errors = tokenValid.Errors.Select(x => x.Description).ToList()
};
}
private async Task<List<Microsoft.AspNetCore.Identity.IdentityResult>> AddRoles(IEnumerable<ClaimCheckboxes> roles, OmbiUser ombiUser)
{
var roleResult = new List<Microsoft.AspNetCore.Identity.IdentityResult>();
@ -404,9 +502,9 @@ namespace Ombi.Controllers
return roleResult;
}
private IdentityResult Error(string message)
private OmbiIdentityResult Error(string message)
{
return new IdentityResult
return new OmbiIdentityResult
{
Errors = new List<string> { message }
};

@ -0,0 +1,9 @@
namespace Ombi.Models.Identity
{
public class ResetPasswordToken
{
public string Token { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Models.Identity
{
public class SubmitPasswordReset
{
public string Email { get; set; }
}
}
Loading…
Cancel
Save