1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

Added cacheing to Client Store, and Management API

This commit is contained in:
Uncled1023 2019-01-28 23:15:00 -08:00
parent b5c02f7640
commit fc86f237fb
9 changed files with 408 additions and 24 deletions

View File

@ -14,10 +14,13 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Teknik.Configuration;
using Teknik.IdentityServer.Models;
using Teknik.IdentityServer.Models.Manage;
using Teknik.IdentityServer.Services;
using Teknik.Logging;
using Teknik.Utilities;
@ -28,17 +31,22 @@ namespace Teknik.IdentityServer.Controllers
[ApiController]
public class ManageController : DefaultController
{
private const string _UserInfoCacheKey = "UserInfo";
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IMemoryCache _cache;
public ManageController(
ILogger<Logger> logger,
Config config,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) : base(logger, config)
SignInManager<ApplicationUser> signInManager,
IMemoryCache cache) : base(logger, config)
{
_userManager = userManager;
_signInManager = signInManager;
_cache = cache;
}
[HttpPost]
@ -91,7 +99,11 @@ namespace Teknik.IdentityServer.Controllers
var result = await _userManager.DeleteAsync(foundUser);
if (result.Succeeded)
{
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to delete user.", identityErrors = result.Errors });
}
@ -104,8 +116,8 @@ namespace Teknik.IdentityServer.Controllers
{
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
return new JsonResult(new { success = true, data = foundUser != null });
}
@ -115,12 +127,11 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
return new JsonResult(new { success = true, data = foundUser.ToJson() });
}
return new JsonResult(new { success = false, message = "User does not exist." });
}
@ -132,7 +143,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Password))
return new JsonResult(new { success = false, message = "Password is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
bool valid = await _userManager.CheckPasswordAsync(foundUser, model.Password);
@ -148,7 +159,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
string token = await _userManager.GeneratePasswordResetTokenAsync(foundUser);
@ -168,7 +179,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Password))
return new JsonResult(new { success = false, message = "Password is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.ResetPasswordAsync(foundUser, model.Token, model.Password);
@ -191,7 +202,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.NewPassword))
return new JsonResult(new { success = false, message = "New Password is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.ChangePasswordAsync(foundUser, model.CurrentPassword, model.NewPassword);
@ -210,12 +221,15 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.SetEmailAsync(foundUser, model.Email);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
var token = await _userManager.GenerateEmailConfirmationTokenAsync(foundUser);
return new JsonResult(new { success = true, data = token });
}
@ -234,9 +248,12 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Token))
return new JsonResult(new { success = false, message = "Token is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
var result = await _userManager.ConfirmEmailAsync(foundUser, model.Token);
if (result.Succeeded)
return new JsonResult(new { success = true });
@ -253,14 +270,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.AccountStatus = model.AccountStatus;
var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update account status.", identityErrors = result.Errors });
}
@ -274,14 +296,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.AccountType = model.AccountType;
var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update account type.", identityErrors = result.Errors });
}
@ -295,14 +322,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.PGPPublicKey = model.PGPPublicKey;
var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update pgp public key.", identityErrors = result.Errors });
}
@ -316,7 +348,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);
@ -333,9 +365,12 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
await _userManager.ResetAuthenticatorKeyAsync(foundUser);
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);
@ -353,7 +388,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Code))
return new JsonResult(new { success = false, message = "Code is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Strip spaces and hypens
@ -367,6 +402,9 @@ namespace Teknik.IdentityServer.Controllers
var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, true);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
}
@ -386,12 +424,17 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, false);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to disable Two-Factor Authentication.", identityErrors = result.Errors });
}
@ -405,11 +448,14 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
if (foundUser.TwoFactorEnabled)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
@ -607,5 +653,34 @@ namespace Teknik.IdentityServer.Controllers
return result.ToString().ToLowerInvariant();
}
private async Task<ApplicationUser> GetCachedUser(string username)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentNullException("username");
// Check the cache
string cacheKey = _UserInfoCacheKey + username;
ApplicationUser foundUser;
if (!_cache.TryGetValue(cacheKey, out foundUser))
{
foundUser = await _userManager.FindByNameAsync(username);
if (foundUser != null)
{
_cache.AddToCache(cacheKey, foundUser, new TimeSpan(1, 0, 0));
}
}
return foundUser;
}
private void RemoveCachedUser(string username)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentNullException("username");
string cacheKey = _UserInfoCacheKey + username;
_cache.Remove(cacheKey);
}
}
}

View File

@ -0,0 +1,244 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Teknik.IdentityServer;
namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190129061223_UserEditDate")]
partial class UserEditDate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<int>("AccountStatus");
b.Property<int>("AccountType");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<DateTime>("CreationDate");
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<DateTime>("LastEdit");
b.Property<DateTime>("LastSeen");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PGPPublicKey");
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
public partial class UserEditDate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastEdit",
table: "AspNetUsers",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastEdit",
table: "AspNetUsers");
}
}
}

View File

@ -15,7 +15,7 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-preview2-35157")
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
@ -150,6 +150,8 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.Property<bool>("EmailConfirmed");
b.Property<DateTime>("LastEdit");
b.Property<DateTime>("LastSeen");
b.Property<bool>("LockoutEnabled");

View File

@ -21,6 +21,8 @@ namespace Teknik.IdentityServer.Models
public string PGPPublicKey { get; set; }
public DateTime LastEdit { get; set; }
public ApplicationUser() : base()
{
Init();
@ -35,6 +37,7 @@ namespace Teknik.IdentityServer.Models
{
CreationDate = DateTime.Now;
LastSeen = DateTime.Now;
LastEdit = DateTime.Now;
AccountType = AccountType.Basic;
AccountStatus = AccountStatus.Active;
PGPPublicKey = null;
@ -46,6 +49,7 @@ namespace Teknik.IdentityServer.Models
claims.Add(new Claim("username", UserName));
claims.Add(new Claim("creation-date", CreationDate.ToString("o")));
claims.Add(new Claim("last-seen", LastSeen.ToString("o")));
claims.Add(new Claim("last-edit", LastEdit.ToString("o")));
claims.Add(new Claim("account-type", AccountType.ToString()));
claims.Add(new Claim("account-status", AccountStatus.ToString()));
claims.Add(new Claim("recovery-email", Email ?? string.Empty));
@ -62,6 +66,7 @@ namespace Teknik.IdentityServer.Models
new JProperty("username", UserName),
new JProperty("creation-date", CreationDate),
new JProperty("last-seen", LastSeen),
new JProperty("last-edit", LastEdit),
new JProperty("account-type", AccountType),
new JProperty("account-status", AccountStatus),
new JProperty("recovery-email", Email),

View File

@ -23,8 +23,7 @@ using Teknik.Logging;
using Microsoft.AspNetCore.Authorization;
using Teknik.IdentityServer.Models;
using IdentityServer4.Services;
using System.Collections.Generic;
using Teknik.Utilities;
namespace Teknik.IdentityServer
{
@ -128,6 +127,7 @@ namespace Teknik.IdentityServer
options.Cors.CorsPaths.Add(new PathString("/connect/endsession"));
options.Cors.CorsPaths.Add(new PathString("/connect/checksession"));
options.Cors.CorsPaths.Add(new PathString("/connect/introspect"));
options.Caching.ClientStoreExpiration = TimeSpan.FromHours(1);
})
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
@ -135,6 +135,7 @@ namespace Teknik.IdentityServer
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(config.DbConnection, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStoreCache()
.AddAspNetIdentity<ApplicationUser>()
.AddRedirectUriValidator<TeknikRedirectUriValidator>()
.AddDeveloperSigningCredential();

View File

@ -25,4 +25,4 @@
</div>
</div>
<bundle src="js/userInfo.min.js" append-version="true"></bundle>
<bundle src="js/userSearch.min.js" append-version="true"></bundle>

View File

@ -20,7 +20,7 @@
{
"outputFileName": "./wwwroot/js/userSearch.min.js",
"inputFiles": [
"./wwwroot/js/app/Admin/UserInfo.js"
"./wwwroot/js/app/Admin/UserSearch.js"
]
},
{

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Caching.Memory;
namespace Teknik.Utilities
{
public static class MemoryCacheHelper
{
public static bool GetCacheValue<T>(this IMemoryCache cache, string key, out T value)
{
return cache.TryGetValue(key, out value);
}
public static bool GetCacheValue(this IMemoryCache cache, string key, out object value)
{
return cache.TryGetValue(key, out value);
}
public static void AddToCache(this IMemoryCache cache, string key, object value, TimeSpan expiration)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expiration);
cache.AddToCache(key, value, cacheOptions);
}
public static void AddToCache(this IMemoryCache cache, string key, object value, MemoryCacheEntryOptions options)
{
cache.Set(key, value, options);
}
}
}