Add JWT, OTP, and caching features
Upgraded projects to .NET 9.0 and added new projects `KBR.Cache`, `KBR.Shared`, and `KBR.Shared.Lite` to the solution. Introduced JWT authentication and OTP handling with new models, services, and configuration options. Updated database schema with new entities `Users` and `RefreshTokens`, and added migrations for schema changes. Implemented caching strategies using `AppDistributedCache` with support for in-memory, SQL Server, and Redis. Enhanced email handling with `MailHelpers` for domain replacement. Updated controllers, repositories, and configuration files to support new features.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
|
||||
using KBR.Shared.Cache;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace KBR.Cache
|
||||
{
|
||||
internal class AppDistributedCache : IAppDistributedCache
|
||||
{
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly DistributedCacheEntryOptions _entryOptions;
|
||||
|
||||
public AppDistributedCache(IDistributedCache cache)
|
||||
{
|
||||
this._cache = cache;
|
||||
this._entryOptions = new DistributedCacheEntryOptions();
|
||||
}
|
||||
|
||||
public T Get<T>(string key)
|
||||
where T : class
|
||||
{
|
||||
if (this._cache.Get(key) is var result && result != null)
|
||||
{
|
||||
return result.FromByteArray<T>();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
where T : class
|
||||
{
|
||||
var result = await this._cache.GetAsync(key, cancellationToken);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result.FromByteArray<T>();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public void Set<T>(string key, T obj, DistributedCacheEntryOptions? entryOptions = default)
|
||||
{
|
||||
this._cache.Set(key, obj.ToByteArray(), entryOptions ?? this._entryOptions);
|
||||
}
|
||||
|
||||
public async Task SetAsync<T>(string key, T obj, DistributedCacheEntryOptions? entryOptions = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this._cache.SetAsync(key, obj.ToByteArray(), entryOptions ?? this._entryOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
this._cache.Remove(key);
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this._cache.RemoveAsync(key, cancellationToken);
|
||||
}
|
||||
|
||||
public TItem GetOrCreate<TItem>(string key, Func<TItem> factory)
|
||||
where TItem : class
|
||||
{
|
||||
byte[] result = this._cache.Get(key);
|
||||
var resultData = result?.FromByteArray<TItem>();
|
||||
if (result == null || resultData == null)
|
||||
{
|
||||
var data = factory();
|
||||
this._cache.Set(key, data.ToByteArray(), this._entryOptions);
|
||||
return data;
|
||||
}
|
||||
|
||||
return resultData;
|
||||
}
|
||||
|
||||
public async Task<TItem> GetOrCreateAsync<TItem>(string key, Func<Task<TItem>> factory, CancellationToken cancellationToken = default)
|
||||
where TItem : class
|
||||
{
|
||||
byte[] result = await this._cache.GetAsync(key, cancellationToken);
|
||||
var resultData = result?.FromByteArray<TItem>();
|
||||
if (result == null || resultData == null)
|
||||
{
|
||||
var data = await factory();
|
||||
if (data != null)
|
||||
{
|
||||
await this._cache.SetAsync(key, data.ToByteArray(), this._entryOptions, cancellationToken);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return resultData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using KBR.Shared.Cache.Models;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace KBR.Cache
|
||||
{
|
||||
public static class CacheHelpers
|
||||
{
|
||||
private static readonly string _userOtpSecrets = nameof(_userOtpSecrets);
|
||||
|
||||
private static string UserOtpSecretKey(string key) => $"{_userOtpSecrets}-{key}";
|
||||
|
||||
public static async Task SetUserSecretsCacheAsync(IAppDistributedCache cache, string userId, string secret, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await cache.SetAsync(UserOtpSecretKey(userId), secret, new DistributedCacheEntryOptions { AbsoluteExpiration = DateTime.UtcNow.AddMinutes(3) }, cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<string> GetUserOtpSecretAsync(IAppDistributedCache cache, string userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = await cache.GetAsync<string>(UserOtpSecretKey(userId), cancellationToken);
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace KBR.Cache.Constants
|
||||
{
|
||||
public static class CacheProviders
|
||||
{
|
||||
public const string InMemory = nameof(InMemory);
|
||||
|
||||
public const string SqlServer = nameof(SqlServer);
|
||||
|
||||
public const string Redis = nameof(Redis);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using KBR.Cache.Constants;
|
||||
using KBR.Cache.Options;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace KBR.Cache.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCache([NotNull] this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Ensure the 'Microsoft.Extensions.Options.ConfigurationExtensions' package is referenced in your project
|
||||
var cacheOptions = configuration.GetSection("Cache").Get<CacheOptions>();
|
||||
|
||||
if (cacheOptions == null || cacheOptions.Provider == CacheProviders.InMemory)
|
||||
{
|
||||
services.AddDistributedMemoryCache();
|
||||
}
|
||||
else if (cacheOptions.Provider == CacheProviders.SqlServer)
|
||||
{
|
||||
services.AddDistributedMySqlCache(options =>
|
||||
{
|
||||
options.ConnectionString = cacheOptions.ConnectionString;
|
||||
options.TableName = "Cache";
|
||||
});
|
||||
}
|
||||
else if (cacheOptions.Provider == CacheProviders.Redis)
|
||||
{
|
||||
services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = cacheOptions.ConnectionString;
|
||||
options.InstanceName = "SampleInstance";
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Cache options are not valid");
|
||||
}
|
||||
|
||||
services.TryAddSingleton<IAppDistributedCache, AppDistributedCache>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace KBR.Cache
|
||||
{
|
||||
public interface IAppDistributedCache
|
||||
{
|
||||
T Get<T>(string key)
|
||||
where T : class;
|
||||
|
||||
Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
where T : class;
|
||||
|
||||
void Remove(string key);
|
||||
|
||||
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
|
||||
|
||||
void Set<T>(string key, T obj, DistributedCacheEntryOptions? options = default);
|
||||
|
||||
Task SetAsync<T>(string key, T obj, DistributedCacheEntryOptions? options = default, CancellationToken cancellationToken = default);
|
||||
|
||||
TItem GetOrCreate<TItem>(string key, Func<TItem> factory)
|
||||
where TItem : class;
|
||||
|
||||
Task<TItem> GetOrCreateAsync<TItem>(string key, Func<Task<TItem>> factory, CancellationToken cancellationToken = default)
|
||||
where TItem : class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.9" />
|
||||
<PackageReference Include="Pomelo.Extensions.Caching.MySql" Version="2.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KBR.Share.Lite\KBR.Shared.Lite.csproj" />
|
||||
<ProjectReference Include="..\KBR.Shared\KBR.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace KBR.Cache.Options
|
||||
{
|
||||
public class CacheOptions
|
||||
{
|
||||
public string Provider { get; set; } = null!;
|
||||
|
||||
public string? ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KBR.Shared.Lite.Extensions
|
||||
{
|
||||
public static class JsonExtensions
|
||||
{
|
||||
public static T? ToObject<T>(string value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
public static T? ToObjectSystemText<T>(string value)
|
||||
{
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(value);
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(string value, Type destType)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject(value, destType);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return (T)result;
|
||||
}
|
||||
|
||||
public static object? ToObject(string value, Type destType)
|
||||
{
|
||||
return ToObject<object?>(value, destType);
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(string value, JsonConverter[] converters)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value, converters);
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(string value, Type destType, JsonConverter[] converters)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject(value, destType, converters);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return (T)result;
|
||||
}
|
||||
|
||||
public static string ToJson<T>(T value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value);
|
||||
}
|
||||
|
||||
public static string ToJsonSystemText<T>(T value)
|
||||
{
|
||||
return System.Text.Json.JsonSerializer.Serialize(value);
|
||||
}
|
||||
|
||||
public static string ToJson<T>(T value, JsonSerializerSettings jsonSerializerSettings)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, jsonSerializerSettings);
|
||||
}
|
||||
|
||||
public static byte[] ToUtf8Bytes(object obj)
|
||||
{
|
||||
return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(obj);
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(byte[] utf8Json)
|
||||
{
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(utf8Json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
using KBR.Shared.Lite.Extensions;
|
||||
|
||||
namespace KBR.Shared.Cache
|
||||
{
|
||||
public static class CacheExtensions
|
||||
{
|
||||
public static byte[] ToByteArray(this object obj)
|
||||
{
|
||||
if (obj is not null)
|
||||
{
|
||||
return JsonExtensions.ToUtf8Bytes(obj);
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public static T? FromByteArray<T>(this byte[] byteArray)
|
||||
where T : class
|
||||
{
|
||||
if (byteArray is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonExtensions.ToObject<T>(byteArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using KBR.Shared.Cache.Models;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace KBR.Shared.Cache
|
||||
{
|
||||
public static class CacheHelpers
|
||||
{
|
||||
private static readonly string _userSessions = nameof(_userSessions);
|
||||
private static string UserSessionsKey(string key) => $"{_userSessions}-{key}";
|
||||
|
||||
public static async Task SetUserSessionsCacheAsync(IAppDistributedCache cache, string userId, UserSessionModel sessions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await cache.SetAsync(UserSessionsKey(userId), sessions, GetOptions(), cancellationToken);
|
||||
}
|
||||
|
||||
private static DistributedCacheEntryOptions GetOptions()
|
||||
{
|
||||
return new DistributedCacheEntryOptions { AbsoluteExpiration = DateTime.MaxValue };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace KBR.Shared.Cache
|
||||
{
|
||||
public interface IAppDistributedCache
|
||||
{
|
||||
T Get<T>(string key)
|
||||
where T : class;
|
||||
|
||||
Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
where T : class;
|
||||
|
||||
void Remove(string key);
|
||||
|
||||
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
|
||||
|
||||
void Set<T>(string key, T obj, DistributedCacheEntryOptions? options = default);
|
||||
|
||||
Task SetAsync<T>(string key, T obj, DistributedCacheEntryOptions? options = default, CancellationToken cancellationToken = default);
|
||||
|
||||
TItem GetOrCreate<TItem>(string key, Func<TItem> factory)
|
||||
where TItem : class;
|
||||
|
||||
Task<TItem> GetOrCreateAsync<TItem>(string key, Func<Task<TItem>> factory, CancellationToken cancellationToken = default)
|
||||
where TItem : class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace KBR.Shared.Cache.Models
|
||||
{
|
||||
public class UserSessionModel
|
||||
{
|
||||
public UserSessionModel()
|
||||
{
|
||||
Alive = new();
|
||||
Killed = new();
|
||||
}
|
||||
|
||||
public List<string> Alive { get; set; }
|
||||
|
||||
public List<string> Killed { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KBR.Share.Lite\KBR.Shared.Lite.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace KBR.Shared.Mail
|
||||
{
|
||||
public static class MailHelpers
|
||||
{
|
||||
// Replace email with other domain passed as a parameter
|
||||
public static string ReplaceEmailDomain(string email, string newDomain)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
throw new ArgumentException("Email cannot be null or empty.", nameof(email));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newDomain))
|
||||
throw new ArgumentException("New domain cannot be null or empty.", nameof(newDomain));
|
||||
|
||||
var atIndex = email.IndexOf('@');
|
||||
if (atIndex == -1)
|
||||
throw new ArgumentException("Invalid email format.", nameof(email));
|
||||
|
||||
return $"{email.Substring(0, atIndex + 1)}{newDomain}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user