From 51fbd56c4471f4172604fd634ca581100ce35fcb Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 13 Jul 2017 15:23:39 +0100 Subject: [PATCH] Finished implimenting Identity with IdentityServer4. #865 #1456 --- .../Rule/Request/AutoApproveRuleTests.cs | 8 +- .../Rule/Request/CanRequestRuleTests.cs | 12 +- .../Claims/{OmbiClaims.cs => OmbiRoles.cs} | 2 +- src/Ombi.Core/Helpers/EmailValidator.cs | 62 +++ .../Rule/Rules/Request/AutoApproveRule.cs | 6 +- .../Rule/Rules/Request/CanRequestRule.cs | 6 +- .../Rule/Rules/Specific/CanRequestRule.cs | 2 +- src/Ombi.Store/Context/OmbiContext.cs | 6 +- src/Ombi/Attributes/PowerUserAttribute.cs | 4 +- src/Ombi/ClientApp/app/app.component.html | 2 +- src/Ombi/ClientApp/app/auth/auth.service.ts | 5 +- src/Ombi/ClientApp/app/interfaces/IUser.ts | 10 + .../app/services/identity.service.ts | 15 +- .../emailnotification.component.html | 2 +- .../updatedetails.component.html | 47 +++ .../usermanagement/updatedetails.component.ts | 61 +++ .../usermanagement-add.component.html | 21 +- .../usermanagement-add.component.ts | 33 +- .../usermanagement-edit.component.html | 3 +- .../usermanagement-edit.component.ts | 45 +- .../usermanagement.component.ts | 59 +-- .../usermanagement/usermanagement.module.ts | 8 +- src/Ombi/Controllers/IdentityController.cs | 391 +++++++++++++----- src/Ombi/IdentityConfig.cs | 5 +- src/Ombi/Models/Identity/IdentityResult.cs | 10 + .../Models/Identity/UpdateLocalUserModel.cs | 10 + 26 files changed, 631 insertions(+), 204 deletions(-) rename src/Ombi.Core/Claims/{OmbiClaims.cs => OmbiRoles.cs} (92%) create mode 100644 src/Ombi.Core/Helpers/EmailValidator.cs create mode 100644 src/Ombi/ClientApp/app/usermanagement/updatedetails.component.html create mode 100644 src/Ombi/ClientApp/app/usermanagement/updatedetails.component.ts create mode 100644 src/Ombi/Models/Identity/IdentityResult.cs create mode 100644 src/Ombi/Models/Identity/UpdateLocalUserModel.cs diff --git a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs index e3fdda556..558a7c850 100644 --- a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs @@ -22,7 +22,7 @@ namespace Ombi.Core.Tests.Rule.Request [Fact] public async Task Should_ReturnSuccess_WhenAdminAndRequestMovie() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.Admin)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -33,7 +33,7 @@ namespace Ombi.Core.Tests.Rule.Request [Fact] public async Task Should_ReturnSuccess_WhenAdminAndRequestTV() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.Admin)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -44,7 +44,7 @@ namespace Ombi.Core.Tests.Rule.Request [Fact] public async Task Should_ReturnSuccess_WhenAutoApproveMovieAndRequestMovie() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.AutoApproveMovie)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.AutoApproveMovie)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -55,7 +55,7 @@ namespace Ombi.Core.Tests.Rule.Request [Fact] public async Task Should_ReturnSuccess_WhenAutoApproveTVAndRequestTV() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.AutoApproveTv)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.AutoApproveTv)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); diff --git a/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs index cc5ebebee..3610ccc6c 100644 --- a/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs @@ -23,7 +23,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnSuccess_WhenRequestingMovieWithMovieRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.RequestMovie)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestMovie)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -33,7 +33,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnFail_WhenRequestingMovieWithoutMovieRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.RequestMovie)).Returns(false); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestMovie)).Returns(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -44,7 +44,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnSuccess_WhenRequestingMovieWithAdminRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.Admin)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -54,7 +54,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnSuccess_WhenRequestingTVWithAdminRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.Admin)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -64,7 +64,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnSuccess_WhenRequestingTVWithTVRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.RequestTv)).Returns(true); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestTv)).Returns(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -74,7 +74,7 @@ namespace Ombi.Core.Tests.Rule [Fact] public async Task Should_ReturnFail_WhenRequestingTVWithoutTVRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiClaims.RequestTv)).Returns(false); + PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestTv)).Returns(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); diff --git a/src/Ombi.Core/Claims/OmbiClaims.cs b/src/Ombi.Core/Claims/OmbiRoles.cs similarity index 92% rename from src/Ombi.Core/Claims/OmbiClaims.cs rename to src/Ombi.Core/Claims/OmbiRoles.cs index 0c0c4bb10..15ce52f9a 100644 --- a/src/Ombi.Core/Claims/OmbiClaims.cs +++ b/src/Ombi.Core/Claims/OmbiRoles.cs @@ -1,6 +1,6 @@ namespace Ombi.Core.Claims { - public static class OmbiClaims + public static class OmbiRoles { public const string Admin = nameof(Admin); public const string AutoApproveMovie = nameof(AutoApproveMovie); diff --git a/src/Ombi.Core/Helpers/EmailValidator.cs b/src/Ombi.Core/Helpers/EmailValidator.cs new file mode 100644 index 000000000..f310d5b08 --- /dev/null +++ b/src/Ombi.Core/Helpers/EmailValidator.cs @@ -0,0 +1,62 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Ombi.Core.Helpers +{ + public static class EmailValidator + { + static bool _invalid; + + public static bool IsValidEmail(string strIn) + { + _invalid = false; + if (string.IsNullOrEmpty(strIn)) + return false; + + // Use IdnMapping class to convert Unicode domain names. + try + { + strIn = Regex.Replace(strIn, "(@)(.+)$", DomainMapper, + RegexOptions.None, TimeSpan.FromMilliseconds(200)); + } + catch (RegexMatchTimeoutException) + { + return false; + } + + if (_invalid) + return false; + + // Return true if strIn is in valid e-mail format. + try + { + return Regex.IsMatch(strIn, + @"^(?("")("".+?(? Execute(BaseRequest obj) { - if (User.IsInRole(OmbiClaims.Admin)) + if (User.IsInRole(OmbiRoles.Admin)) { obj.Approved = true; return Task.FromResult(Success()); } - if (obj.RequestType == RequestType.Movie && User.IsInRole(OmbiClaims.AutoApproveMovie)) + if (obj.RequestType == RequestType.Movie && User.IsInRole(OmbiRoles.AutoApproveMovie)) obj.Approved = true; - if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiClaims.AutoApproveTv)) + if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiRoles.AutoApproveTv)) obj.Approved = true; return Task.FromResult(Success()); // We don't really care, we just don't set the obj to approve } diff --git a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs index a8630f368..79e202162 100644 --- a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs @@ -18,17 +18,17 @@ namespace Ombi.Core.Rule.Rules public Task Execute(BaseRequest obj) { - if (User.IsInRole(OmbiClaims.Admin)) + if (User.IsInRole(OmbiRoles.Admin)) return Task.FromResult(Success()); if (obj.RequestType == RequestType.Movie) { - if (User.IsInRole(OmbiClaims.RequestMovie)) + if (User.IsInRole(OmbiRoles.RequestMovie)) return Task.FromResult(Success()); return Task.FromResult(Fail("You do not have permissions to Request a Movie")); } - if (User.IsInRole(OmbiClaims.RequestTv)) + if (User.IsInRole(OmbiRoles.RequestTv)) return Task.FromResult(Success()); return Task.FromResult(Fail("You do not have permissions to Request a Movie")); } diff --git a/src/Ombi.Core/Rule/Rules/Specific/CanRequestRule.cs b/src/Ombi.Core/Rule/Rules/Specific/CanRequestRule.cs index 536212992..36f4492af 100644 --- a/src/Ombi.Core/Rule/Rules/Specific/CanRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Specific/CanRequestRule.cs @@ -21,7 +21,7 @@ namespace Ombi.Core.Rule.Rules.Specific var req = (BaseRequest)obj; var sendNotification = !req.Approved; /*|| !prSettings.IgnoreNotifyForAutoApprovedRequests;*/ - if (User.IsInRole(OmbiClaims.Admin)) + if (User.IsInRole(OmbiRoles.Admin)) sendNotification = false; // Don't bother sending a notification if the user is an admin return Task.FromResult(new RuleResult { diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 491732f80..d48d6a5b9 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -36,7 +36,11 @@ namespace Ombi.Store.Context public DbSet MovieIssues { get; set; } public DbSet TvIssues { get; set; } public DbSet EmailTokens { get; set; } - + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/src/Ombi/Attributes/PowerUserAttribute.cs b/src/Ombi/Attributes/PowerUserAttribute.cs index 854a071af..bc0fc0e14 100644 --- a/src/Ombi/Attributes/PowerUserAttribute.cs +++ b/src/Ombi/Attributes/PowerUserAttribute.cs @@ -9,8 +9,8 @@ namespace Ombi.Attributes { var roles = new [] { - OmbiClaims.Admin, - OmbiClaims.PowerUser + OmbiRoles.Admin, + OmbiRoles.PowerUser }; Roles = string.Join(",",roles); } diff --git a/src/Ombi/ClientApp/app/app.component.html b/src/Ombi/ClientApp/app/app.component.html index 80b50eee4..e3fcb3860 100644 --- a/src/Ombi/ClientApp/app/app.component.html +++ b/src/Ombi/ClientApp/app/app.component.html @@ -34,7 +34,7 @@ diff --git a/src/Ombi/ClientApp/app/auth/auth.service.ts b/src/Ombi/ClientApp/app/auth/auth.service.ts index 0f81ca289..dd7bb66b4 100644 --- a/src/Ombi/ClientApp/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/app/auth/auth.service.ts @@ -44,8 +44,8 @@ export class AuthService extends ServiceHelpers { throw "Invalid token"; } var json = this.jwtHelper.decodeToken(token); - var roles = json["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"] - var name = json["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]; + var roles = json["role"]; + var name = json["name"]; var u = { name: name, roles: [] as string[] }; @@ -64,7 +64,6 @@ export class AuthService extends ServiceHelpers { logout() { localStorage.removeItem('id_token'); - localStorage.removeItem('currentUser'); } } diff --git a/src/Ombi/ClientApp/app/interfaces/IUser.ts b/src/Ombi/ClientApp/app/interfaces/IUser.ts index 4071b6354..0f8ae3576 100644 --- a/src/Ombi/ClientApp/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/app/interfaces/IUser.ts @@ -20,3 +20,13 @@ export interface ICheckbox { enabled: boolean, } +export interface IIdentityResult { + errors: string[], + successful: boolean, +} + +export interface IUpdateLocalUser extends IUser { + currentPassword: string, + confirmNewPassword: string +} + diff --git a/src/Ombi/ClientApp/app/services/identity.service.ts b/src/Ombi/ClientApp/app/services/identity.service.ts index 4d499b5e9..4c2752076 100644 --- a/src/Ombi/ClientApp/app/services/identity.service.ts +++ b/src/Ombi/ClientApp/app/services/identity.service.ts @@ -4,7 +4,7 @@ import { Http } from '@angular/http'; import { Observable } from 'rxjs/Rx'; import { ServiceAuthHelpers } from './service.helpers'; -import { IUser, ICheckbox } from '../interfaces/IUser'; +import { IUser, IUpdateLocalUser, ICheckbox, IIdentityResult } from '../interfaces/IUser'; @Injectable() @@ -20,7 +20,7 @@ export class IdentityService extends ServiceAuthHelpers { return this.http.get(this.url).map(this.extractData); } - getUserById(id: number): Observable { + getUserById(id: string): Observable { return this.http.get(`${this.url}User/${id}`).map(this.extractData); } @@ -32,12 +32,19 @@ export class IdentityService extends ServiceAuthHelpers { return this.http.get(`${this.url}Claims`).map(this.extractData); } - createUser(user: IUser): Observable { + createUser(user: IUser): Observable { return this.http.post(this.url, JSON.stringify(user), { headers: this.headers }).map(this.extractData); } - updateUser(user: IUser): Observable { + updateUser(user: IUser): Observable { return this.http.put(this.url, JSON.stringify(user), { headers: this.headers }).map(this.extractData); + } + updateLocalUser(user: IUpdateLocalUser): Observable { + return this.http.put(this.url + 'local', JSON.stringify(user), { headers: this.headers }).map(this.extractData); + } + + deleteUser(user: IUser): Observable { + return this.http.delete(`${this.url}/${user.id}`, { headers: this.headers }).map(this.extractData); } hasRole(role: string): boolean { diff --git a/src/Ombi/ClientApp/app/settings/notifications/emailnotification.component.html b/src/Ombi/ClientApp/app/settings/notifications/emailnotification.component.html index cef2acf38..ab99f01a4 100644 --- a/src/Ombi/ClientApp/app/settings/notifications/emailnotification.component.html +++ b/src/Ombi/ClientApp/app/settings/notifications/emailnotification.component.html @@ -22,7 +22,7 @@
Host is required
The Port is required
The Email Sender is required
-
The Email Sender needs to be a valid email address
+
The Email Sender needs to be a valid email address
The Email Sender is required
The Admin Email needs to be a valid email address
The Username is required
diff --git a/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.html b/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.html new file mode 100644 index 000000000..f76f01959 --- /dev/null +++ b/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.html @@ -0,0 +1,47 @@ +
+ +

Hello {{form.value.username}}!

+
+
+ +
+ + +
+
+
+
+
+
Email address format is incorrect
+
The Password is required
+
The Confirm New Password is required
+
Your current passowrd is required
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.ts b/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.ts new file mode 100644 index 000000000..f449b27df --- /dev/null +++ b/src/Ombi/ClientApp/app/usermanagement/updatedetails.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, Validators, FormBuilder } from '@angular/forms'; + +import { IUpdateLocalUser } from '../interfaces/IUser'; +import { IdentityService } from '../services/identity.service'; +import { NotificationService } from '../services/notification.service'; + +@Component({ + templateUrl: './updatedetails.component.html' +}) +export class UpdateDetailsComponent implements OnInit { + constructor(private identityService: IdentityService, + private notificationService: NotificationService, + private fb: FormBuilder) { } + + form: FormGroup; + + ngOnInit(): void { + this.identityService.getUser().subscribe(x => { + var localUser = x as IUpdateLocalUser; + this.form = this.fb.group({ + id:[localUser.id], + username: [localUser.username], + emailAddress: [localUser.emailAddress, [Validators.email]], + confirmNewPassword: [localUser.confirmNewPassword], + currentPassword: [localUser.currentPassword, [Validators.required]], + password: [localUser.password], + }); + + + }); + + } + + onSubmit(form : FormGroup) { + if (form.invalid) { + this.notificationService.error("Validation", "Please check your entered values"); + return + } + + if (form.controls["password"].dirty) { + if (form.value.password !== form.value.confirmNewPassword) { + this.notificationService.error("Error", "Passwords do not match"); + return; + } + } + + this.identityService.updateLocalUser(this.form.value).subscribe(x => { + if (x.successful) { + this.notificationService.success("Updated", `All of your details have now been updated`) + } else { + x.errors.forEach((val) => { + this.notificationService.error("Error", val); + }); + } + }); + + } + + +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.html b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.html index 946a4bc22..2f47365d4 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.html +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.html @@ -18,10 +18,24 @@
- +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
@@ -31,12 +45,9 @@
- - -
- +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts index c5a94da8f..18a60ac54 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts @@ -12,10 +12,11 @@ import { NotificationService } from '../services/notification.service'; export class UserManagementAddComponent implements OnInit { constructor(private identityService: IdentityService, private notificationSerivce: NotificationService, - private router : Router) { } + private router: Router) { } user: IUser; availableClaims: ICheckbox[]; + confirmPass: ""; ngOnInit(): void { this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x); @@ -30,11 +31,35 @@ export class UserManagementAddComponent implements OnInit { } } - update(): void { + create(): void { this.user.claims = this.availableClaims; + + if (this.user.password) { + if (this.user.password !== this.confirmPass) { + this.notificationSerivce.error("Error", "Passwords do not match"); + return; + } + } + var hasClaims = this.availableClaims.some((item) => { + if (item.enabled) { return true; } + + return false; + }); + + if (!hasClaims) { + this.notificationSerivce.error("Error", "Please assign a role"); + return; + } + this.identityService.createUser(this.user).subscribe(x => { - this.notificationSerivce.success("Updated", `The user ${this.user.username} has been created successfully`) - this.router.navigate(['usermanagement']); + if (x.successful) { + this.notificationSerivce.success("Updated", `The user ${this.user.username} has been created successfully`) + this.router.navigate(['usermanagement']); + } else { + x.errors.forEach((val) => { + this.notificationSerivce.error("Error", val); + }); + } }) } diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html index 0d8a68195..3d0c22b97 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html @@ -37,7 +37,8 @@
- + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts index 906081c19..546b97cc3 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { IUser } from '../interfaces/IUser'; import { IdentityService } from '../services/identity.service'; @@ -11,10 +12,11 @@ import { ActivatedRoute } from '@angular/router'; export class UserManagementEditComponent { constructor(private identityService: IdentityService, private route: ActivatedRoute, - private notificationSerivce: NotificationService) { + private notificationSerivce: NotificationService, + private router: Router) { this.route.params .subscribe(params => { - this.userId = +params['id']; // (+) converts string 'id' to a number + this.userId = params['id']; this.identityService.getUserById(this.userId).subscribe(x => { this.user = x; @@ -23,13 +25,44 @@ export class UserManagementEditComponent { } user: IUser; - userId: number; + userId: string; + + delete(): void { + this.identityService.deleteUser(this.user).subscribe(x => { + if (x.successful) { + this.notificationSerivce.success("Deleted", `The user ${this.user.username} was deleted`) + this.router.navigate(['usermanagement']); + } else { + x.errors.forEach((val) => { + this.notificationSerivce.error("Error", val); + }); + } + + }); + } update(): void { - this.identityService.updateUser(this.user).subscribe(x => { + var hasClaims = this.user.claims.some((item) => { + if (item.enabled) { return true; } - this.notificationSerivce.success("Updated",`The user ${this.user.username} has been updated successfully`) + return false; + }); + + if (!hasClaims) { + this.notificationSerivce.error("Error", "Please assign a role"); + return; + } + + this.identityService.updateUser(this.user).subscribe(x => { + if (x.successful) { + this.notificationSerivce.success("Updated", `The user ${this.user.username} has been updated successfully`) + this.router.navigate(['usermanagement']); + } else { + x.errors.forEach((val) => { + this.notificationSerivce.error("Error", val); + }); + } }) } - + } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts index d5526eac0..2f9f706bd 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { IUser, ICheckbox } from '../interfaces/IUser'; +import { IUser } from '../interfaces/IUser'; import { IdentityService } from '../services/identity.service'; @Component({ @@ -15,65 +15,10 @@ export class UserManagementComponent implements OnInit { this.identityService.getUsers().subscribe(x => { this.users = x; }); - this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x); - this.resetCreatedUser(); } users: IUser[]; - selectedUser: IUser; - createdUser: IUser; - - availableClaims : ICheckbox[]; - - showEditDialog = false; - showCreateDialogue = false; - edit(user: IUser) { - this.selectedUser = user; - this.showEditDialog = true; - } - - updateUser() { - this.showEditDialog = false; - this.identityService.updateUser(this.selectedUser).subscribe(x => this.selectedUser = x); - } - - create() { - this.createdUser.claims = this.availableClaims; - this.identityService.createUser(this.createdUser).subscribe(x => { - this.users.push(x); // Add the new user - - this.showCreateDialogue = false; - this.resetCreatedUser(); - }); - - } - - private resetClaims() { - //this.availableClaims.forEach(x => { - // x.enabled = false; - //}); - } - - private resetCreatedUser() { - this.createdUser = { - id: "-1", - alias: "", - claims: [], - emailAddress: "", - password: "", - userType: 1, - username: "", - - } - this.resetClaims(); - } - - //private removeRequestFromUi(key : IRequestModel) { - // var index = this.requests.indexOf(key, 0); - // if (index > -1) { - // this.requests.splice(index, 1); - // } - //} + } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts index a94f7b5aa..21074b64e 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts @@ -1,13 +1,14 @@ import { NgModule, } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { UserManagementComponent } from './usermanagement.component'; import { UserManagementEditComponent } from './usermanagement-edit.component'; import { UserManagementAddComponent } from './usermanagement-add.component'; +import { UpdateDetailsComponent } from './updatedetails.component'; import { IdentityService } from '../services/identity.service'; @@ -17,19 +18,22 @@ const routes: Routes = [ { path: 'usermanagement', component: UserManagementComponent, canActivate: [AuthGuard] }, { path: 'usermanagement/add', component: UserManagementAddComponent, canActivate: [AuthGuard] }, { path: 'usermanagement/edit/:id', component: UserManagementEditComponent, canActivate: [AuthGuard] }, + { path: 'usermanagement/updatedetails', component: UpdateDetailsComponent, canActivate: [AuthGuard] }, ]; @NgModule({ imports: [ CommonModule, FormsModule, + ReactiveFormsModule, RouterModule.forChild(routes), NgbModule.forRoot(), ], declarations: [ UserManagementComponent, UserManagementAddComponent, - UserManagementEditComponent + UserManagementEditComponent, + UpdateDetailsComponent ], exports: [ RouterModule diff --git a/src/Ombi/Controllers/IdentityController.cs b/src/Ombi/Controllers/IdentityController.cs index 3e3d4302d..34b36073e 100644 --- a/src/Ombi/Controllers/IdentityController.cs +++ b/src/Ombi/Controllers/IdentityController.cs @@ -1,18 +1,23 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; + using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; + using Ombi.Attributes; using Ombi.Core.Claims; +using Ombi.Core.Helpers; using Ombi.Core.Models.UI; using Ombi.Models; +using Ombi.Models.Identity; using Ombi.Store.Entities; +using IdentityResult = Ombi.Models.Identity.IdentityResult; namespace Ombi.Controllers { @@ -34,16 +39,6 @@ namespace Ombi.Controllers private RoleManager RoleManager { get; } private IMapper Mapper { get; } - /// - /// Gets the current user. - /// - /// Information about the current user - [HttpGet] - public async Task GetUser() - { - return Mapper.Map(await UserManager.GetUserAsync(User)); - } - /// /// This is what the Wizard will call when creating the user for the very first time. /// This should never be called after this. @@ -68,29 +63,18 @@ namespace Ombi.Controllers var userToCreate = new OmbiUser { UserName = user.Username, - + UserType = UserType.LocalUser }; - + var result = await UserManager.CreateAsync(userToCreate, user.Password); if (result.Succeeded) { - if (!(await RoleManager.RoleExistsAsync("Admin"))) + if (!await RoleManager.RoleExistsAsync(OmbiRoles.Admin)) { - var r = await RoleManager.CreateAsync(new IdentityRole("Admin")); + await RoleManager.CreateAsync(new IdentityRole(OmbiRoles.Admin)); } - var re = await UserManager.AddToRoleAsync(userToCreate, "Admin"); - - var v = User.IsInRole("Admin"); - - + await UserManager.AddToRoleAsync(userToCreate, OmbiRoles.Admin); } - //await UserManager.CreateUser(new UserDto - //{ - // Username = user.Username, - // UserType = UserType.LocalUser, - // Claims = new List() { new Claim(ClaimTypes.Role, OmbiClaims.Admin) }, - // Password = user.Password, - //}); return true; } @@ -102,30 +86,30 @@ namespace Ombi.Controllers [HttpGet("Users")] public async Task> GetAllUsers() { - var type = typeof(OmbiClaims); - FieldInfo[] fieldInfos = type.GetFields(BindingFlags.Public | - BindingFlags.Static | BindingFlags.FlattenHierarchy); + var users = await UserManager.Users + .ToListAsync(); - var fields = fieldInfos.Where(fi => fi.IsLiteral && !fi.IsInitOnly).ToList(); - var allClaims = fields.Select(x => x.Name).ToList(); - var users = Mapper.Map>(UserManager.Users).ToList(); + var model = new List(); foreach (var user in users) { - var userClaims = user.Claims.Select(x => x.Value); - var left = allClaims.Except(userClaims); - - foreach (var c in left) - { - user.Claims.Add(new ClaimCheckboxes - { - Enabled = false, - Value = c - }); - } + model.Add(await GetUserWithRoles(user)); } - return users; + return model; + } + + /// + /// Gets the current logged in user. + /// + /// Information about all users + [HttpGet] + [Authorize] + public async Task GetCurrentUser() + { + var user = await UserManager.GetUserAsync(User); + + return await GetUserWithRoles(user); } /// @@ -133,86 +117,299 @@ namespace Ombi.Controllers /// /// Information about the user [HttpGet("User/{id}")] - public async Task GetUser(int id) + public async Task GetUser(string id) { - var type = typeof(OmbiClaims); - FieldInfo[] fieldInfos = type.GetFields(BindingFlags.Public | - BindingFlags.Static | BindingFlags.FlattenHierarchy); + var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == id); - var fields = fieldInfos.Where(fi => fi.IsLiteral && !fi.IsInitOnly).ToList(); - var allClaims = fields.Select(x => x.Name).ToList(); - var user = Mapper.Map(await UserManager.Users.FirstOrDefaultAsync(x => x.Id == id.ToString())); + return await GetUserWithRoles(user); + } - - var userClaims = user.Claims.Select(x => x.Value); - IEnumerable left = allClaims.Except(userClaims); - - foreach (var c in left) + private async Task GetUserWithRoles(OmbiUser user) + { + var userRoles = await UserManager.GetRolesAsync(user); + var vm = new UserViewModel { - user.Claims.Add(new ClaimCheckboxes + Alias = user.Alias, + Username = user.UserName, + Id = user.Id, + EmailAddress = user.Email, + UserType = (Core.Models.UserType)(int)user.UserType, + Claims = new List(), + }; + + foreach (var role in userRoles) + { + vm.Claims.Add(new ClaimCheckboxes { - Enabled = false, - Value = c + Value = role, + Enabled = true }); } + // Add the missing claims + var allRoles = await RoleManager.Roles.ToListAsync(); + var missing = allRoles.Select(x => x.Name).Except(userRoles); + foreach (var role in missing) + { + vm.Claims.Add(new ClaimCheckboxes + { + Value = role, + Enabled = false + }); + } - return user; + return vm; } /// /// Creates the user. /// - /// The user. + /// The user. /// - //[HttpPost] - //public async Task CreateUser([FromBody] UserViewModel user) - //{ - // user.Id = null; - // var userResult = await UserManager.CreateUser(Mapper.Map(user)); - // return Mapper.Map(userResult); - //} + [HttpPost] + public async Task CreateUser([FromBody] UserViewModel user) + { + if (!EmailValidator.IsValidEmail(user.EmailAddress)) + { + return Error($"The email address {user.EmailAddress} is not a valid format"); + } + var ombiUser = new OmbiUser + { + Alias = user.Alias, + Email = user.EmailAddress, + UserName = user.Username, + UserType = UserType.LocalUser, + }; + var userResult = await UserManager.CreateAsync(ombiUser, user.Password); + + if (!userResult.Succeeded) + { + // We did not create the user + return new IdentityResult + { + Errors = userResult.Errors.Select(x => x.Description).ToList() + }; + } + + var roleResult = await AddRoles(user.Claims, ombiUser); + + if (roleResult.Any(x => !x.Succeeded)) + { + var messages = new List(); + foreach (var errors in roleResult.Where(x => !x.Succeeded)) + { + messages.AddRange(errors.Errors.Select(x => x.Description).ToList()); + } + + return new IdentityResult + { + Errors = messages + }; + } + + return new IdentityResult + { + Successful = true + }; + } + + /// + /// This is for the local user to change their details. + /// + /// + /// + [HttpPut("local")] + [Authorize] + public async Task UpdateLocalUser([FromBody] UpdateLocalUserModel ui) + { + if (string.IsNullOrEmpty(ui.CurrentPassword)) + { + return Error("You need to provide your current password to make any changes"); + } + + var changingPass = !string.IsNullOrEmpty(ui.Password) || !string.IsNullOrEmpty(ui.ConfirmNewPassword); + + if (changingPass) + { + if (!ui.Password.Equals(ui?.ConfirmNewPassword, StringComparison.CurrentCultureIgnoreCase)) + { + return Error("Passwords do not match"); + } + } + + if (!EmailValidator.IsValidEmail(ui.EmailAddress)) + { + return Error($"The email address {ui.EmailAddress} is not a valid format"); + } + // Get the user + var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == ui.Id); + if (user == null) + { + return Error("The user does not exist"); + } + + // Make sure the pass is ok + var passwordCheck = await UserManager.CheckPasswordAsync(user, ui.CurrentPassword); + if (!passwordCheck) + { + return Error("Your password is incorrect"); + } + + user.Email = ui.EmailAddress; + + var updateResult = await UserManager.UpdateAsync(user); + if (!updateResult.Succeeded) + { + return new IdentityResult + { + Errors = updateResult.Errors.Select(x => x.Description).ToList() + }; + } + + if (changingPass) + { + var result = await UserManager.ChangePasswordAsync(user, ui.CurrentPassword, ui.Password); + + if (!result.Succeeded) + { + return new IdentityResult + { + Errors = result.Errors.Select(x => x.Description).ToList() + }; + } + } + return new IdentityResult + { + Successful = true + }; + + } /// /// Updates the user. /// - /// The user. + /// The user. /// - //[HttpPut] - //public async Task UpdateUser([FromBody] UserViewModel user) - //{ - // var userResult = await UserManager.UpdateUser(Mapper.Map(user)); - // return Mapper.Map(userResult); - //} + [HttpPut] + public async Task UpdateUser([FromBody] UserViewModel ui) + { + if (!EmailValidator.IsValidEmail(ui.EmailAddress)) + { + return Error($"The email address {ui.EmailAddress} is not a valid format"); + } + // Get the user + var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == ui.Id); + user.Alias = ui.Alias; + user.Email = ui.EmailAddress; + var updateResult = await UserManager.UpdateAsync(user); + if (!updateResult.Succeeded) + { + return new IdentityResult + { + Errors = updateResult.Errors.Select(x => x.Description).ToList() + }; + } - ///// - ///// Deletes the user. - ///// - ///// The user. - ///// - //[HttpDelete] - //public async Task DeleteUser([FromBody] UserViewModel user) - //{ - // await UserManager.DeleteUser(Mapper.Map(user)); - // return Ok(); - //} + // Get the roles + var userRoles = await UserManager.GetRolesAsync(user); + + foreach (var role in userRoles) + { + await UserManager.RemoveFromRoleAsync(user, role); + } + + var result = await AddRoles(ui.Claims, user); + if (result.Any(x => !x.Succeeded)) + { + var messages = new List(); + foreach (var errors in result.Where(x => !x.Succeeded)) + { + messages.AddRange(errors.Errors.Select(x => x.Description).ToList()); + } + + return new IdentityResult + { + Errors = messages + }; + } + + return new IdentityResult + { + Successful = true + }; + + } + + /// + /// Deletes the user. + /// + /// The user. + /// + [HttpDelete("{userId}")] + public async Task DeleteUser(string userId) + { + + var userToDelete = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == userId); + if (userToDelete != null) + { + var result = await UserManager.DeleteAsync(userToDelete); + if (result.Succeeded) + { + return new IdentityResult + { + Successful = true + }; + } + + return new IdentityResult + { + Errors = result.Errors.Select(x => x.Description).ToList() + }; + } + return Error("Could not find user to delete."); + } /// /// Gets all available claims in the system. /// /// [HttpGet("claims")] - public IEnumerable GetAllClaims() + public async Task> GetAllClaims() { - var type = typeof(OmbiClaims); - FieldInfo[] fieldInfos = type.GetFields(BindingFlags.Public | - BindingFlags.Static | BindingFlags.FlattenHierarchy); - - var fields = fieldInfos.Where(fi => fi.IsLiteral && !fi.IsInitOnly).ToList(); - var allClaims = fields.Select(x => x.Name).ToList(); - - return allClaims.Select(x => new ClaimCheckboxes() { Value = x }); + var claims = new List(); + // Add the missing claims + var allRoles = await RoleManager.Roles.ToListAsync(); + var missing = allRoles.Select(x => x.Name); + foreach (var role in missing) + { + claims.Add(new ClaimCheckboxes + { + Value = role, + Enabled = false + }); + } + return claims; } + private async Task> AddRoles(IEnumerable roles, OmbiUser ombiUser) + { + var roleResult = new List(); + foreach (var role in roles) + { + if (role.Enabled) + { + roleResult.Add(await UserManager.AddToRoleAsync(ombiUser, role.Value)); + } + } + return roleResult; + } + + private IdentityResult Error(string message) + { + return new IdentityResult + { + Errors = new List { message } + }; + } } } diff --git a/src/Ombi/IdentityConfig.cs b/src/Ombi/IdentityConfig.cs index f943e6f83..59b8b5605 100644 --- a/src/Ombi/IdentityConfig.cs +++ b/src/Ombi/IdentityConfig.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using IdentityModel; using IdentityServer4.Models; namespace Ombi @@ -25,8 +26,8 @@ namespace Ombi { new ApiResource("api", "API") { - UserClaims = {"role", "name"}, - + UserClaims = {JwtClaimTypes.Name, JwtClaimTypes.Role, JwtClaimTypes.Email, JwtClaimTypes.Id}, + } }; } diff --git a/src/Ombi/Models/Identity/IdentityResult.cs b/src/Ombi/Models/Identity/IdentityResult.cs new file mode 100644 index 000000000..b1a96dae5 --- /dev/null +++ b/src/Ombi/Models/Identity/IdentityResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ombi.Models.Identity +{ + public class IdentityResult + { + public List Errors { get; set; } + public bool Successful { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi/Models/Identity/UpdateLocalUserModel.cs b/src/Ombi/Models/Identity/UpdateLocalUserModel.cs new file mode 100644 index 000000000..23306af4c --- /dev/null +++ b/src/Ombi/Models/Identity/UpdateLocalUserModel.cs @@ -0,0 +1,10 @@ +using Ombi.Core.Models.UI; + +namespace Ombi.Models.Identity +{ + public class UpdateLocalUserModel : UserViewModel + { + public string CurrentPassword { get; set; } + public string ConfirmNewPassword { get; set; } + } +} \ No newline at end of file