After the first try literally gave me a head ace, there is the second try with a better way of structuring it and not divinding so much core components in individual features. Still not done though
This commit is contained in:
188
Moonlight/Core/Services/IdentityService.cs
Normal file
188
Moonlight/Core/Services/IdentityService.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Exceptions;
|
||||
using Moonlight.Core.Helpers;
|
||||
using Moonlight.Core.Models.Abstractions;
|
||||
using Moonlight.Core.Models.Enums;
|
||||
using Moonlight.Core.Repositories;
|
||||
using Moonlight.Core.Services.Utils;
|
||||
using Moonlight.Features.StoreSystem.Entities;
|
||||
using OtpNet;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
// This service allows you to reauthenticate, login and force login
|
||||
// It does also contain the permission system accessor for the current user
|
||||
public class IdentityService
|
||||
{
|
||||
private readonly Repository<User> UserRepository;
|
||||
private readonly JwtService JwtService;
|
||||
|
||||
private string Token;
|
||||
|
||||
public User? CurrentUserNullable { get; private set; }
|
||||
public User CurrentUser => CurrentUserNullable!;
|
||||
public bool IsSignedIn => CurrentUserNullable != null;
|
||||
public FlagStorage Flags { get; private set; } = new("");
|
||||
public PermissionStorage Permissions { get; private set; } = new(-1);
|
||||
public Transaction[] Transactions => GetTransactions().Result; // TODO: make more efficient
|
||||
public EventHandler OnAuthenticationStateChanged { get; set; }
|
||||
|
||||
public IdentityService(Repository<User> userRepository,
|
||||
JwtService jwtService)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
JwtService = jwtService;
|
||||
}
|
||||
|
||||
// Transactions
|
||||
public Task<Transaction[]> GetTransactions()
|
||||
{
|
||||
if (CurrentUserNullable == null)
|
||||
return Task.FromResult(Array.Empty<Transaction>());
|
||||
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.Include(x => x.Transactions)
|
||||
.First(x => x.Id == CurrentUserNullable.Id);
|
||||
|
||||
return Task.FromResult(user.Transactions.ToArray());
|
||||
}
|
||||
|
||||
// Authentication
|
||||
|
||||
public async Task Authenticate() // Reauthenticate
|
||||
{
|
||||
// Save the last id (or -1 if not set) so we can track a change
|
||||
var lastUserId = CurrentUserNullable == null ? -1 : CurrentUserNullable.Id;
|
||||
|
||||
// Reset
|
||||
CurrentUserNullable = null;
|
||||
|
||||
await ValidateToken();
|
||||
|
||||
// Get current user id to compare against the last one
|
||||
var currentUserId = CurrentUserNullable == null ? -1 : CurrentUserNullable.Id;
|
||||
|
||||
if (lastUserId != currentUserId) // State changed, lets notify all event listeners
|
||||
OnAuthenticationStateChanged?.Invoke(this, null!);
|
||||
}
|
||||
|
||||
private async Task ValidateToken() // Read and validate token
|
||||
{
|
||||
if (string.IsNullOrEmpty(Token))
|
||||
return;
|
||||
|
||||
if (!await JwtService.Validate(Token))
|
||||
return;
|
||||
|
||||
var data = await JwtService.Decode(Token);
|
||||
|
||||
if (!data.ContainsKey("userId"))
|
||||
return;
|
||||
|
||||
var userId = int.Parse(data["userId"]);
|
||||
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
if (!data.ContainsKey("issuedAt"))
|
||||
return;
|
||||
|
||||
var issuedAt = long.Parse(data["issuedAt"]);
|
||||
var issuedAtDateTime = DateTimeOffset.FromUnixTimeSeconds(issuedAt).DateTime;
|
||||
|
||||
// If the valid time is newer then when the token was issued, the token is not longer valid
|
||||
if (user.TokenValidTimestamp > issuedAtDateTime)
|
||||
return;
|
||||
|
||||
CurrentUserNullable = user;
|
||||
|
||||
if (CurrentUserNullable == null) // If the current user is null, stop loading additional data
|
||||
return;
|
||||
|
||||
Flags = new(CurrentUser.Flags);
|
||||
Permissions = new(CurrentUser.Permissions);
|
||||
}
|
||||
|
||||
public async Task<string> Login(string email, string password, string? code = null)
|
||||
{
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Email == email);
|
||||
|
||||
if (user == null)
|
||||
throw new DisplayException("A user with these credential combination was not found");
|
||||
|
||||
if (!HashHelper.Verify(password, user.Password))
|
||||
throw new DisplayException("A user with these credential combination was not found");
|
||||
|
||||
var flags = new FlagStorage(user.Flags); // Construct FlagStorage to check for 2fa
|
||||
|
||||
if (!flags[UserFlag.TotpEnabled]) // No 2fa found on this user so were done here
|
||||
return await GenerateToken(user);
|
||||
|
||||
// If we reach this point, 2fa is enabled so we need to continue validating
|
||||
|
||||
if (string.IsNullOrEmpty(code)) // This will show an additional 2fa login field
|
||||
throw new ArgumentNullException(nameof(code), "2FA code missing");
|
||||
|
||||
if (user.TotpKey == null) // Hopefully we will never fulfill this check ;)
|
||||
throw new DisplayException("2FA key is missing. Please contact the support to fix your account");
|
||||
|
||||
// Calculate server side code
|
||||
var totp = new Totp(Base32Encoding.ToBytes(user.TotpKey));
|
||||
var codeServerSide = totp.ComputeTotp();
|
||||
|
||||
if (codeServerSide == code)
|
||||
return await GenerateToken(user);
|
||||
|
||||
throw new DisplayException("Invalid 2fa code entered");
|
||||
}
|
||||
|
||||
public async Task<string> GenerateToken(User user)
|
||||
{
|
||||
var token = await JwtService.Create(data =>
|
||||
{
|
||||
data.Add("userId", user.Id.ToString());
|
||||
data.Add("issuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
|
||||
}, TimeSpan.FromDays(10));
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public Task SaveFlags()
|
||||
{
|
||||
// Prevent saving flags for an empty user
|
||||
if (!IsSignedIn)
|
||||
return Task.CompletedTask;
|
||||
|
||||
// Save the new flag string
|
||||
CurrentUser.Flags = Flags.RawFlagString;
|
||||
UserRepository.Update(CurrentUser);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Helpers and overloads
|
||||
public async Task
|
||||
Authenticate(HttpRequest request) // Overload for api controllers to authenticate a user like the normal panel
|
||||
{
|
||||
if (request.Cookies.ContainsKey("token"))
|
||||
{
|
||||
var token = request.Cookies["token"];
|
||||
await Authenticate(token!);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Authenticate(string token) // Overload to set token and reauth
|
||||
{
|
||||
Token = token;
|
||||
await Authenticate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user