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:
53
Moonlight/Core/Services/Background/AutoMailSendService.cs
Normal file
53
Moonlight/Core/Services/Background/AutoMailSendService.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Event;
|
||||
using Moonlight.Core.Event.Args;
|
||||
|
||||
namespace Moonlight.Core.Services.Background;
|
||||
|
||||
public class AutoMailSendService // This service is responsible for sending mails automatically
|
||||
{
|
||||
private readonly MailService MailService;
|
||||
|
||||
public AutoMailSendService(MailService mailService)
|
||||
{
|
||||
MailService = mailService;
|
||||
|
||||
Events.OnUserRegistered += OnUserRegistered;
|
||||
Events.OnServiceOrdered += OnServiceOrdered;
|
||||
Events.OnTransactionCreated += OnTransactionCreated;
|
||||
}
|
||||
|
||||
private async void OnTransactionCreated(object? sender, TransactionCreatedEventArgs eventArgs)
|
||||
{
|
||||
await MailService.Send(
|
||||
eventArgs.User,
|
||||
"New transaction",
|
||||
"transactionCreated",
|
||||
eventArgs.Transaction,
|
||||
eventArgs.User
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnServiceOrdered(object? _, Service service)
|
||||
{
|
||||
await MailService.Send(
|
||||
service.Owner,
|
||||
"New product ordered",
|
||||
"serviceOrdered",
|
||||
service,
|
||||
service.Product,
|
||||
service.Owner
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnUserRegistered(object? _, User user)
|
||||
{
|
||||
await MailService.Send(
|
||||
user,
|
||||
$"Welcome {user.Username}",
|
||||
"welcome",
|
||||
user
|
||||
);
|
||||
}
|
||||
}
|
||||
84
Moonlight/Core/Services/BucketService.cs
Normal file
84
Moonlight/Core/Services/BucketService.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Moonlight.Core.Helpers;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
public class BucketService
|
||||
{
|
||||
private readonly string BasePath;
|
||||
public string[] Buckets => GetBuckets();
|
||||
|
||||
|
||||
public BucketService()
|
||||
{
|
||||
// This is used to create the buckets folder in the persistent storage of helio
|
||||
BasePath = PathBuilder.Dir("storage", "buckets");
|
||||
Directory.CreateDirectory(BasePath);
|
||||
}
|
||||
|
||||
public string[] GetBuckets()
|
||||
{
|
||||
return Directory
|
||||
.GetDirectories(BasePath)
|
||||
.Select(x =>
|
||||
x.Replace(BasePath, "").TrimEnd('/')
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Task EnsureBucket(string name) // To ensure a specific bucket has been created, call this function
|
||||
{
|
||||
Directory.CreateDirectory(PathBuilder.Dir(BasePath, name));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> Store(string bucket, Stream dataStream, string fileName)
|
||||
{
|
||||
await EnsureBucket(bucket); // Ensure the bucket actually exists
|
||||
|
||||
// Create a safe to file name to store the file
|
||||
var extension = Path.GetExtension(fileName);
|
||||
var finalFileName = Path.GetRandomFileName() + extension;
|
||||
var finalFilePath = PathBuilder.File(BasePath, bucket, finalFileName);
|
||||
|
||||
// Copy the file from the remote stream to the bucket
|
||||
var fs = File.Create(finalFilePath);
|
||||
await dataStream.CopyToAsync(fs);
|
||||
await fs.FlushAsync();
|
||||
fs.Close();
|
||||
|
||||
// Return the generated file name to save it in the db or smth
|
||||
return finalFileName;
|
||||
}
|
||||
|
||||
public Task<Stream> Pull(string bucket, string file)
|
||||
{
|
||||
var filePath = PathBuilder.File(BasePath, bucket, file);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var stream = File.Open(filePath, FileMode.Open);
|
||||
|
||||
return Task.FromResult<Stream>(stream);
|
||||
}
|
||||
else
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
public Task Delete(string bucket, string file, bool ignoreNotFound = false)
|
||||
{
|
||||
var filePath = PathBuilder.File(BasePath, bucket, file);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// This section will only be reached if the file does not exist
|
||||
|
||||
if (!ignoreNotFound)
|
||||
throw new FileNotFoundException();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
102
Moonlight/Core/Services/ConfigService.cs
Normal file
102
Moonlight/Core/Services/ConfigService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Moonlight.Core.Configuration;
|
||||
using Moonlight.Core.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
public class ConfigService
|
||||
{
|
||||
private readonly string Path = PathBuilder.File("storage", "config.json");
|
||||
private ConfigV1 Data;
|
||||
|
||||
public ConfigService()
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
if(!File.Exists(Path))
|
||||
File.WriteAllText(Path, "{}");
|
||||
|
||||
var text = File.ReadAllText(Path);
|
||||
Data = JsonConvert.DeserializeObject<ConfigV1>(text) ?? new();
|
||||
|
||||
ApplyEnvironmentVariables("Moonlight", Data);
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var text = JsonConvert.SerializeObject(Data, Formatting.Indented);
|
||||
File.WriteAllText(Path, text);
|
||||
}
|
||||
|
||||
public ConfigV1 Get()
|
||||
{
|
||||
return Data;
|
||||
}
|
||||
|
||||
public string GetDiagnoseJson()
|
||||
{
|
||||
var text = File.ReadAllText(Path);
|
||||
var data = JsonConvert.DeserializeObject<ConfigV1>(text) ?? new();
|
||||
|
||||
// Security token
|
||||
data.Security.Token = "";
|
||||
|
||||
// Database
|
||||
if (string.IsNullOrEmpty(data.Database.Password))
|
||||
data.Database.Password = "WAS EMPTY";
|
||||
else
|
||||
data.Database.Password = "WAS NOT EMPTY";
|
||||
|
||||
// Mailserver
|
||||
if (string.IsNullOrEmpty(data.MailServer.Password))
|
||||
data.MailServer.Password = "WAS EMPTY";
|
||||
else
|
||||
data.MailServer.Password = "WAS NOT EMPTY";
|
||||
|
||||
return JsonConvert.SerializeObject(data, Formatting.Indented);
|
||||
}
|
||||
|
||||
private void ApplyEnvironmentVariables(string prefix, object objectToLookAt)
|
||||
{
|
||||
foreach (var property in objectToLookAt.GetType().GetProperties())
|
||||
{
|
||||
var envName = $"{prefix}_{property.Name}";
|
||||
|
||||
if (property.PropertyType.Assembly == GetType().Assembly)
|
||||
{
|
||||
ApplyEnvironmentVariables(envName, property.GetValue(objectToLookAt)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!Environment.GetEnvironmentVariables().Contains(envName))
|
||||
continue;
|
||||
|
||||
var envValue = Environment.GetEnvironmentVariable(envName)!;
|
||||
|
||||
if (property.PropertyType == typeof(string))
|
||||
{
|
||||
property.SetValue(objectToLookAt, envValue);
|
||||
}
|
||||
else if (property.PropertyType == typeof(int))
|
||||
{
|
||||
if(!int.TryParse(envValue, out int envIntValue))
|
||||
continue;
|
||||
|
||||
property.SetValue(objectToLookAt, envIntValue);
|
||||
}
|
||||
else if (property.PropertyType == typeof(bool))
|
||||
{
|
||||
if(!bool.TryParse(envValue, out bool envBoolValue))
|
||||
continue;
|
||||
|
||||
property.SetValue(objectToLookAt, envBoolValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
29
Moonlight/Core/Services/Interop/AdBlockService.cs
Normal file
29
Moonlight/Core/Services/Interop/AdBlockService.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.JSInterop;
|
||||
using Moonlight.Core.Helpers;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class AdBlockService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public AdBlockService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<bool> Detect()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<bool>("moonlight.utils.vendo"); // lat. vendo = advertisement xd
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn("An unexpected error occured while trying to detect possible ad blockers");
|
||||
Logger.Warn(e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Moonlight/Core/Services/Interop/AlertService.cs
Normal file
50
Moonlight/Core/Services/Interop/AlertService.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class AlertService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public AlertService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task Info(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.info", title, message);
|
||||
}
|
||||
|
||||
public async Task Success(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.success", title, message);
|
||||
}
|
||||
|
||||
public async Task Warning(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.warning", title, message);
|
||||
}
|
||||
|
||||
public async Task Error(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.error", title, message);
|
||||
}
|
||||
|
||||
public async Task<string> Text(string title, string message)
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<string>("moonlight.alerts.text", title, message);
|
||||
}
|
||||
|
||||
public async Task<bool> YesNo(string title, string yes, string no)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<bool>("moonlight.alerts.yesno", title, yes, no);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Moonlight/Core/Services/Interop/CookieService.cs
Normal file
63
Moonlight/Core/Services/Interop/CookieService.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class CookieService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
private string Expires = "";
|
||||
|
||||
public CookieService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
ExpireDays = 300;
|
||||
}
|
||||
|
||||
public async Task SetValue(string key, string value, int? days = null)
|
||||
{
|
||||
var curExp = (days != null) ? (days > 0 ? DateToUTC(days.Value) : "") : Expires;
|
||||
await SetCookie($"{key}={value}; expires={curExp}; path=/");
|
||||
}
|
||||
|
||||
public async Task<string> GetValue(string key, string def = "")
|
||||
{
|
||||
var cookieString = await GetCookie();
|
||||
|
||||
var cookieParts = cookieString.Split(";");
|
||||
|
||||
foreach (var cookiePart in cookieParts)
|
||||
{
|
||||
if(string.IsNullOrEmpty(cookiePart))
|
||||
continue;
|
||||
|
||||
var cookieKeyValue = cookiePart.Split("=")
|
||||
.Select(x => x.Trim()) // There may be spaces e.g. with the "AspNetCore.Culture" key
|
||||
.ToArray();
|
||||
|
||||
if (cookieKeyValue.Length == 2)
|
||||
{
|
||||
if (cookieKeyValue[0] == key)
|
||||
return cookieKeyValue[1];
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
private async Task SetCookie(string value)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{value}\"");
|
||||
}
|
||||
|
||||
private async Task<string> GetCookie()
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<string>("eval", $"document.cookie");
|
||||
}
|
||||
|
||||
private int ExpireDays
|
||||
{
|
||||
set => Expires = DateToUTC(value);
|
||||
}
|
||||
|
||||
private static string DateToUTC(int days) => DateTime.Now.AddDays(days).ToUniversalTime().ToString("R");
|
||||
}
|
||||
34
Moonlight/Core/Services/Interop/FileDownloadService.cs
Normal file
34
Moonlight/Core/Services/Interop/FileDownloadService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Text;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class FileDownloadService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public FileDownloadService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task DownloadStream(string fileName, Stream stream)
|
||||
{
|
||||
using var streamRef = new DotNetStreamReference(stream);
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.utils.download", fileName, streamRef);
|
||||
}
|
||||
|
||||
public async Task DownloadBytes(string fileName, byte[] bytes)
|
||||
{
|
||||
var ms = new MemoryStream(bytes);
|
||||
|
||||
await DownloadStream(fileName, ms);
|
||||
|
||||
ms.Close();
|
||||
await ms.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task DownloadString(string fileName, string content) =>
|
||||
await DownloadBytes(fileName, Encoding.UTF8.GetBytes(content));
|
||||
}
|
||||
37
Moonlight/Core/Services/Interop/ModalService.cs
Normal file
37
Moonlight/Core/Services/Interop/ModalService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class ModalService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public ModalService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task Show(string id, bool focus = true) // Focus can be specified to fix issues with other components
|
||||
{
|
||||
try
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id, focus);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Hide(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.modals.hide", id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Moonlight/Core/Services/Interop/ToastService.cs
Normal file
55
Moonlight/Core/Services/Interop/ToastService.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.Core.Services.Interop;
|
||||
|
||||
public class ToastService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public ToastService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task Success(string title, string message, int timeout = 5000)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.toasts.success", title, message, timeout);
|
||||
}
|
||||
|
||||
public async Task Info(string title, string message, int timeout = 5000)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.toasts.info", title, message, timeout);
|
||||
}
|
||||
|
||||
public async Task Danger(string title, string message, int timeout = 5000)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.toasts.danger", title, message, timeout);
|
||||
}
|
||||
|
||||
public async Task Warning(string title, string message, int timeout = 5000)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.toasts.warning", title, message, timeout);
|
||||
}
|
||||
|
||||
// Overloads
|
||||
|
||||
public async Task Success(string message, int timeout = 5000)
|
||||
{
|
||||
await Success("", message, timeout);
|
||||
}
|
||||
|
||||
public async Task Info(string message, int timeout = 5000)
|
||||
{
|
||||
await Info("", message, timeout);
|
||||
}
|
||||
|
||||
public async Task Danger(string message, int timeout = 5000)
|
||||
{
|
||||
await Danger("", message, timeout);
|
||||
}
|
||||
|
||||
public async Task Warning(string message, int timeout = 5000)
|
||||
{
|
||||
await Warning("", message, timeout);
|
||||
}
|
||||
}
|
||||
103
Moonlight/Core/Services/MailService.cs
Normal file
103
Moonlight/Core/Services/MailService.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Helpers;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
public class MailService
|
||||
{
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly string BasePath;
|
||||
|
||||
public MailService(ConfigService configService)
|
||||
{
|
||||
ConfigService = configService;
|
||||
|
||||
BasePath = PathBuilder.Dir("storage", "mail");
|
||||
Directory.CreateDirectory(BasePath);
|
||||
}
|
||||
|
||||
public async Task Send(User user, string title, string templateName, params object[] models)
|
||||
{
|
||||
var config = ConfigService.Get().MailServer;
|
||||
|
||||
try
|
||||
{
|
||||
// Build mail message
|
||||
var message = new MimeMessage();
|
||||
|
||||
message.From.Add(new MailboxAddress(
|
||||
config.SenderName,
|
||||
config.Email
|
||||
));
|
||||
|
||||
message.To.Add(new MailboxAddress(
|
||||
$"{user.Username}",
|
||||
user.Email
|
||||
));
|
||||
|
||||
message.Subject = Formatter.ProcessTemplating(title, models);
|
||||
|
||||
var body = new BodyBuilder()
|
||||
{
|
||||
HtmlBody = await ParseTemplate(templateName, models)
|
||||
};
|
||||
message.Body = body.ToMessageBody();
|
||||
|
||||
// The actual sending will not be done in the mail thread to prevent long loading times
|
||||
Task.Run(async () =>
|
||||
{
|
||||
using var smtpClient = new SmtpClient();
|
||||
|
||||
try
|
||||
{
|
||||
await smtpClient.ConnectAsync(config.Host, config.Port, config.UseSsl);
|
||||
await smtpClient.AuthenticateAsync(config.Email, config.Password);
|
||||
await smtpClient.SendAsync(message);
|
||||
await smtpClient.DisconnectAsync(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn("An unexpected error occured while connecting and transferring mail to mailserver");
|
||||
Logger.Warn(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// ignored as we log it anyways in the parse template function
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn("Unhandled error occured during sending mail:");
|
||||
Logger.Warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ParseTemplate(string templateName, params object[] models)
|
||||
{
|
||||
if (!File.Exists(PathBuilder.File(BasePath, templateName + ".html")))
|
||||
{
|
||||
Logger.Warn($"Mail template '{templateName}' is missing. Skipping sending mail");
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
var text = await File.ReadAllTextAsync(
|
||||
PathBuilder.File(BasePath, templateName + ".html")
|
||||
);
|
||||
|
||||
// For details how the templating works, check out the explanation of the ProcessTemplating in the Formatter class
|
||||
text = Formatter.ProcessTemplating(text, models);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
public async Task Send(IEnumerable<User> users, string title, string templateName, params object[] models)
|
||||
{
|
||||
foreach (var user in users)
|
||||
await Send(user, title, templateName, models);
|
||||
}
|
||||
}
|
||||
145
Moonlight/Core/Services/PluginService.cs
Normal file
145
Moonlight/Core/Services/PluginService.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Reflection;
|
||||
using Moonlight.Core.Helpers;
|
||||
using Moonlight.Core.Models.Abstractions.Services;
|
||||
using Moonlight.Core.Plugins;
|
||||
using Moonlight.Core.Plugins.Contexts;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
public class PluginService
|
||||
{
|
||||
private readonly List<MoonlightPlugin> Plugins = new();
|
||||
|
||||
public async Task Load(WebApplicationBuilder webApplicationBuilder)
|
||||
{
|
||||
var path = PathBuilder.Dir("storage", "plugins");
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
var files = FindFiles(path)
|
||||
.Where(x => x.EndsWith(".dll"))
|
||||
.ToArray();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFile(PathBuilder.File(Directory.GetCurrentDirectory(), file));
|
||||
|
||||
int plugins = 0;
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.IsSubclassOf(typeof(MoonlightPlugin)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!;
|
||||
|
||||
// Create environment
|
||||
plugin.Context = new PluginContext()
|
||||
{
|
||||
Services = webApplicationBuilder.Services,
|
||||
WebApplicationBuilder = webApplicationBuilder
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await plugin.Enable();
|
||||
|
||||
// After here we can treat the plugin as successfully loaded
|
||||
plugins++;
|
||||
Plugins.Add(plugin);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Unhandled exception while enabling plugin '{type.Name}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Failed to create plugin environment for '{type.Name}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(plugins == 0) // If 0, we can assume that it was a library dll
|
||||
Logger.Info($"Loaded {file} as a library");
|
||||
else
|
||||
Logger.Info($"Loaded {plugins} plugin(s) from {file}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Unable to load assembly from file '{file}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info($"Loaded {Plugins.Count} plugin(s)");
|
||||
}
|
||||
|
||||
public Task<MoonlightPlugin[]> GetLoadedPlugins() => Task.FromResult(Plugins.ToArray());
|
||||
|
||||
public async Task RunPreInit()
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
Logger.Info($"Running pre init tasks for {plugin.GetType().Name}");
|
||||
|
||||
foreach (var preInitTask in plugin.Context.PreInitTasks)
|
||||
await Task.Run(preInitTask);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunPrePost(WebApplication webApplication)
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
// Pass through the dependency injection
|
||||
var scope = webApplication.Services.CreateScope();
|
||||
plugin.Context.Provider = scope.ServiceProvider;
|
||||
plugin.Context.Scope = scope;
|
||||
plugin.Context.WebApplication = webApplication;
|
||||
|
||||
Logger.Info($"Running post init tasks for {plugin.GetType().Name}");
|
||||
|
||||
foreach (var postInitTask in plugin.Context.PostInitTasks)
|
||||
await Task.Run(postInitTask);
|
||||
}
|
||||
}
|
||||
|
||||
public Task BuildUserServiceView(ServiceViewContext context)
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
plugin.Context.BuildUserServiceView?.Invoke(context);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task BuildAdminServiceView(ServiceViewContext context)
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
plugin.Context.BuildAdminServiceView?.Invoke(context);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string[] FindFiles(string dir)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
foreach (var file in Directory.GetFiles(dir))
|
||||
result.Add(file);
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(dir))
|
||||
{
|
||||
result.AddRange(FindFiles(directory));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
81
Moonlight/Core/Services/ServiceManage/ServiceAdminService.cs
Normal file
81
Moonlight/Core/Services/ServiceManage/ServiceAdminService.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Exceptions;
|
||||
using Moonlight.Core.Repositories;
|
||||
using Moonlight.Features.StoreSystem.Entities;
|
||||
|
||||
namespace Moonlight.Core.Services.ServiceManage;
|
||||
|
||||
public class ServiceAdminService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
private readonly ServiceDefinitionService ServiceDefinitionService;
|
||||
|
||||
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory, ServiceDefinitionService serviceDefinitionService)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
ServiceDefinitionService = serviceDefinitionService;
|
||||
}
|
||||
|
||||
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
|
||||
{
|
||||
var impl = ServiceDefinitionService.Get(p);
|
||||
|
||||
// Load models in new scope
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var productRepo = scope.ServiceProvider.GetRequiredService<Repository<Product>>();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var user = userRepo.Get().First(x => x.Id == u.Id);
|
||||
var product = productRepo.Get().First(x => x.Id == p.Id);
|
||||
|
||||
// Create database model
|
||||
var service = new Service()
|
||||
{
|
||||
Product = product,
|
||||
Owner = user,
|
||||
Suspended = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Allow further modifications
|
||||
if(modifyService != null)
|
||||
modifyService.Invoke(service);
|
||||
|
||||
// Add new service in database
|
||||
var finishedService = serviceRepo.Add(service);
|
||||
|
||||
// Call the action for the logic behind the service type
|
||||
await impl.Actions.Create(scope.ServiceProvider, finishedService);
|
||||
|
||||
return finishedService;
|
||||
}
|
||||
|
||||
public async Task Delete(Service s)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
var serviceShareRepo = scope.ServiceProvider.GetRequiredService<Repository<ServiceShare>>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Shares)
|
||||
.FirstOrDefault(x => x.Id == s.Id);
|
||||
|
||||
if (service == null)
|
||||
throw new DisplayException("Service does not exist anymore");
|
||||
|
||||
var impl = ServiceDefinitionService.Get(service);
|
||||
|
||||
await impl.Actions.Delete(scope.ServiceProvider, service);
|
||||
|
||||
foreach (var share in service.Shares.ToArray())
|
||||
{
|
||||
serviceShareRepo.Delete(share);
|
||||
}
|
||||
|
||||
serviceRepo.Delete(service);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Database.Enums;
|
||||
using Moonlight.Core.Models.Abstractions.Services;
|
||||
using Moonlight.Core.Repositories;
|
||||
using Moonlight.Features.StoreSystem.Entities;
|
||||
|
||||
namespace Moonlight.Core.Services.ServiceManage;
|
||||
|
||||
public class ServiceDefinitionService
|
||||
{
|
||||
private readonly Dictionary<ServiceType, ServiceDefinition> ServiceImplementations = new();
|
||||
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public ServiceDefinitionService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public void Register<T>(ServiceType type) where T : ServiceDefinition
|
||||
{
|
||||
var impl = Activator.CreateInstance<T>() as ServiceDefinition;
|
||||
|
||||
if (impl == null)
|
||||
throw new ArgumentException("The provided type is not an service implementation");
|
||||
|
||||
if (ServiceImplementations.ContainsKey(type))
|
||||
throw new ArgumentException($"An implementation for {type} has already been registered");
|
||||
|
||||
ServiceImplementations.Add(type, impl);
|
||||
}
|
||||
|
||||
public ServiceDefinition Get(Service s)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
return Get(service.Product);
|
||||
}
|
||||
|
||||
public ServiceDefinition Get(Product p) => Get(p.Type);
|
||||
|
||||
public ServiceDefinition Get(ServiceType type)
|
||||
{
|
||||
if (!ServiceImplementations.ContainsKey(type))
|
||||
throw new ArgumentException($"No service implementation found for {type}");
|
||||
|
||||
return ServiceImplementations[type];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Models.Abstractions;
|
||||
using Moonlight.Core.Models.Enums;
|
||||
using Moonlight.Core.Repositories;
|
||||
|
||||
namespace Moonlight.Core.Services.ServiceManage;
|
||||
|
||||
public class ServiceManageService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public ServiceManageService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public Task<bool> CheckAccess(Service s, User user)
|
||||
{
|
||||
var permissionStorage = new PermissionStorage(user.Permissions);
|
||||
|
||||
// Is admin?
|
||||
if(permissionStorage[Permission.AdminServices])
|
||||
return Task.FromResult(true);
|
||||
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
// Is owner?
|
||||
if(service.Owner.Id == user.Id)
|
||||
return Task.FromResult(true);
|
||||
|
||||
// Is shared user
|
||||
if(service.Shares.Any(x => x.User.Id == user.Id))
|
||||
return Task.FromResult(true);
|
||||
|
||||
// No match
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<bool> NeedsRenewal(Service s)
|
||||
{
|
||||
// We fetch the service in a new scope wo ensure that we are not caching
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
return Task.FromResult(DateTime.UtcNow > service.RenewAt);
|
||||
}
|
||||
}
|
||||
114
Moonlight/Core/Services/ServiceManage/ServiceService.cs
Normal file
114
Moonlight/Core/Services/ServiceManage/ServiceService.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Exceptions;
|
||||
using Moonlight.Core.Repositories;
|
||||
|
||||
namespace Moonlight.Core.Services.ServiceManage;
|
||||
|
||||
public class ServiceService // This service is used for managing services and create the connection to the actual logic behind a service type
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly Repository<Service> ServiceRepository;
|
||||
private readonly Repository<User> UserRepository;
|
||||
|
||||
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
|
||||
public ServiceDefinitionService Definition => ServiceProvider.GetRequiredService<ServiceDefinitionService>();
|
||||
public ServiceManageService Manage => ServiceProvider.GetRequiredService<ServiceManageService>();
|
||||
|
||||
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository, Repository<User> userRepository)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
ServiceRepository = serviceRepository;
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
public Task<Service[]> Get(User user)
|
||||
{
|
||||
var result = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Where(x => x.Owner.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<Service[]> GetShared(User user)
|
||||
{
|
||||
var result = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Shares.Any(y => y.User.Id == user.Id))
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task AddSharedUser(Service s, string username)
|
||||
{
|
||||
var userToAdd = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Username == username);
|
||||
|
||||
if (userToAdd == null)
|
||||
throw new DisplayException("No user found with this username");
|
||||
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
if (service.Owner.Id == userToAdd.Id)
|
||||
throw new DisplayException("The owner cannot be added as a shared user");
|
||||
|
||||
if (service.Shares.Any(x => x.User.Id == userToAdd.Id))
|
||||
throw new DisplayException("The user has already access to this service");
|
||||
|
||||
service.Shares.Add(new ()
|
||||
{
|
||||
User = userToAdd
|
||||
});
|
||||
|
||||
ServiceRepository.Update(service);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<User[]> GetSharedUsers(Service s)
|
||||
{
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
var result = service.Shares
|
||||
.Select(x => x.User)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task RemoveSharedUser(Service s, User user)
|
||||
{
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
var shareToRemove = service.Shares.FirstOrDefault(x => x.User.Id == user.Id);
|
||||
|
||||
if (shareToRemove == null)
|
||||
throw new DisplayException("This user does not have access to this service");
|
||||
|
||||
service.Shares.Remove(shareToRemove);
|
||||
ServiceRepository.Update(service);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
38
Moonlight/Core/Services/SessionService.cs
Normal file
38
Moonlight/Core/Services/SessionService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Moonlight.Core.Models.Abstractions;
|
||||
|
||||
namespace Moonlight.Core.Services;
|
||||
|
||||
public class SessionService
|
||||
{
|
||||
private readonly List<Session> AllSessions = new();
|
||||
|
||||
public Session[] Sessions => GetSessions();
|
||||
|
||||
public Task Register(Session session)
|
||||
{
|
||||
lock (AllSessions)
|
||||
{
|
||||
AllSessions.Add(session);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Unregister(Session session)
|
||||
{
|
||||
lock (AllSessions)
|
||||
{
|
||||
AllSessions.Remove(session);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Session[] GetSessions()
|
||||
{
|
||||
lock (AllSessions)
|
||||
{
|
||||
return AllSessions.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Moonlight/Core/Services/Sys/MoonlightService.cs
Normal file
81
Moonlight/Core/Services/Sys/MoonlightService.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.IO.Compression;
|
||||
using Moonlight.Core.Event;
|
||||
using Moonlight.Core.Extensions;
|
||||
using Moonlight.Core.Helpers;
|
||||
|
||||
namespace Moonlight.Core.Services.Sys;
|
||||
|
||||
public class MoonlightService // This service can be used to perform strictly panel specific actions
|
||||
{
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public WebApplication Application { get; set; } // Do NOT modify using a plugin
|
||||
public string LogPath { get; set; } // Do NOT modify using a plugin
|
||||
public MoonlightThemeService Theme => ServiceProvider.GetRequiredService<MoonlightThemeService>();
|
||||
|
||||
public MoonlightService(ConfigService configService, IServiceProvider serviceProvider)
|
||||
{
|
||||
ConfigService = configService;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task Restart()
|
||||
{
|
||||
Logger.Info("Restarting moonlight");
|
||||
|
||||
// Notify all users that this instance will restart
|
||||
await Events.OnMoonlightRestart.InvokeAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
|
||||
await Application.StopAsync();
|
||||
}
|
||||
|
||||
public async Task<byte[]> GenerateDiagnoseReport()
|
||||
{
|
||||
var scope = ServiceProvider.CreateScope();
|
||||
|
||||
// Prepare zip file
|
||||
var memoryStream = new MemoryStream();
|
||||
var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, true);
|
||||
|
||||
// Add current log
|
||||
// We need to open the file this way because we need to specify the access and share mode directly
|
||||
// in order to read from a file which is currently written to
|
||||
var fs = File.Open(LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
var sr = new StreamReader(fs);
|
||||
var log = await sr.ReadToEndAsync();
|
||||
sr.Close();
|
||||
fs.Close();
|
||||
|
||||
await zip.AddFromText("log.txt", log);
|
||||
|
||||
// TODO: Add node settings here
|
||||
|
||||
// Add config
|
||||
var config = ConfigService.GetDiagnoseJson();
|
||||
await zip.AddFromText("config.json", config);
|
||||
|
||||
// Make a list of plugins
|
||||
var pluginService = scope.ServiceProvider.GetRequiredService<PluginService>();
|
||||
var plugins = await pluginService.GetLoadedPlugins();
|
||||
var pluginList = "Installed plugins:\n";
|
||||
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
var assembly = plugin.GetType().Assembly;
|
||||
pluginList += $"{assembly.FullName} ({assembly.Location})\n";
|
||||
}
|
||||
|
||||
await zip.AddFromText("pluginList.txt", pluginList);
|
||||
|
||||
// Add more information here
|
||||
|
||||
// Finalize file
|
||||
zip.Dispose();
|
||||
memoryStream.Close();
|
||||
var data = memoryStream.ToArray();
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
51
Moonlight/Core/Services/Sys/MoonlightThemeService.cs
Normal file
51
Moonlight/Core/Services/Sys/MoonlightThemeService.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Mappy.Net;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Models.Abstractions;
|
||||
using Moonlight.Core.Repositories;
|
||||
|
||||
namespace Moonlight.Core.Services.Sys;
|
||||
|
||||
public class MoonlightThemeService
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly ConfigService ConfigService;
|
||||
|
||||
public MoonlightThemeService(IServiceProvider serviceProvider, ConfigService configService)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
ConfigService = configService;
|
||||
}
|
||||
|
||||
public Task<ApplicationTheme[]> GetInstalled()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var themeRepo = scope.ServiceProvider.GetRequiredService<Repository<Theme>>();
|
||||
|
||||
var themes = new List<ApplicationTheme>();
|
||||
|
||||
themes.AddRange(themeRepo
|
||||
.Get()
|
||||
.ToArray()
|
||||
.Select(x => Mapper.Map<ApplicationTheme>(x)));
|
||||
|
||||
if (ConfigService.Get().Theme.EnableDefault)
|
||||
{
|
||||
themes.Insert(0, new()
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Moonlight Default",
|
||||
Author = "MasuOwO",
|
||||
Enabled = true,
|
||||
CssUrl = "/css/theme.css",
|
||||
DonateUrl = "https://ko-fi.com/masuowo"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(themes.ToArray());
|
||||
}
|
||||
|
||||
public async Task<ApplicationTheme[]> GetEnabled() =>
|
||||
(await GetInstalled())
|
||||
.Where(x => x.Enabled)
|
||||
.ToArray();
|
||||
}
|
||||
124
Moonlight/Core/Services/Users/UserAuthService.cs
Normal file
124
Moonlight/Core/Services/Users/UserAuthService.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Event;
|
||||
using Moonlight.Core.Exceptions;
|
||||
using Moonlight.Core.Extensions;
|
||||
using Moonlight.Core.Helpers;
|
||||
using Moonlight.Core.Models.Abstractions;
|
||||
using Moonlight.Core.Models.Enums;
|
||||
using Moonlight.Core.Models.Templates;
|
||||
using Moonlight.Core.Repositories;
|
||||
using Moonlight.Core.Services.Utils;
|
||||
using OtpNet;
|
||||
|
||||
namespace Moonlight.Core.Services.Users;
|
||||
|
||||
public class UserAuthService
|
||||
{
|
||||
private readonly Repository<User> UserRepository;
|
||||
private readonly JwtService JwtService;
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly MailService MailService;
|
||||
|
||||
public UserAuthService(
|
||||
Repository<User> userRepository,
|
||||
JwtService jwtService,
|
||||
ConfigService configService,
|
||||
MailService mailService)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
JwtService = jwtService;
|
||||
ConfigService = configService;
|
||||
MailService = mailService;
|
||||
}
|
||||
|
||||
public async Task<User> Register(string username, string email, string password)
|
||||
{
|
||||
// Event though we have form validation i want to
|
||||
// ensure that at least these basic formatting things are done
|
||||
email = email.ToLower().Trim();
|
||||
username = username.ToLower().Trim();
|
||||
|
||||
// Prevent duplication or username and/or email
|
||||
if (UserRepository.Get().Any(x => x.Email == email))
|
||||
throw new DisplayException("A user with that email does already exist");
|
||||
|
||||
if (UserRepository.Get().Any(x => x.Username == username))
|
||||
throw new DisplayException("A user with that username does already exist");
|
||||
|
||||
var user = new User()
|
||||
{
|
||||
Username = username,
|
||||
Email = email,
|
||||
Password = HashHelper.HashToString(password)
|
||||
};
|
||||
|
||||
var result = UserRepository.Add(user);
|
||||
|
||||
await Events.OnUserRegistered.InvokeAsync(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
user.Password = HashHelper.HashToString(newPassword);
|
||||
user.TokenValidTimestamp = DateTime.UtcNow;
|
||||
UserRepository.Update(user);
|
||||
|
||||
await Events.OnUserPasswordChanged.InvokeAsync(user);
|
||||
}
|
||||
|
||||
public Task SeedTotp(User user)
|
||||
{
|
||||
var key = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
|
||||
|
||||
user.TotpKey = key;
|
||||
UserRepository.Update(user);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SetTotp(User user, bool state)
|
||||
{
|
||||
// Access to flags without identity service
|
||||
var flags = new FlagStorage(user.Flags);
|
||||
flags[UserFlag.TotpEnabled] = state;
|
||||
user.Flags = flags.RawFlagString;
|
||||
|
||||
if (!state)
|
||||
user.TotpKey = null;
|
||||
|
||||
UserRepository.Update(user);
|
||||
|
||||
await Events.OnUserTotpSet.InvokeAsync(user);
|
||||
}
|
||||
|
||||
// Mails
|
||||
|
||||
public async Task SendVerification(User user)
|
||||
{
|
||||
var jwt = await JwtService.Create(data => { data.Add("mailToVerify", user.Email); }, TimeSpan.FromMinutes(10));
|
||||
|
||||
await MailService.Send(user, "Verify your account", "verifyMail", user, new MailVerify()
|
||||
{
|
||||
Url = ConfigService.Get().AppUrl + "/api/auth/verify?token=" + jwt
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendResetPassword(string email)
|
||||
{
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Email == email);
|
||||
|
||||
if (user == null)
|
||||
throw new DisplayException("An account with that email was not found");
|
||||
|
||||
var jwt = await JwtService.Create(data => { data.Add("accountToReset", user.Id.ToString()); });
|
||||
|
||||
await MailService.Send(user, "Password reset for your account", "passwordReset", user, new ResetPassword()
|
||||
{
|
||||
Url = ConfigService.Get().AppUrl + "/api/auth/reset?token=" + jwt
|
||||
});
|
||||
}
|
||||
}
|
||||
170
Moonlight/Core/Services/Users/UserDeleteService.cs
Normal file
170
Moonlight/Core/Services/Users/UserDeleteService.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Database.Entities.Store;
|
||||
using Moonlight.Core.Repositories;
|
||||
using Moonlight.Core.Services.ServiceManage;
|
||||
using Moonlight.Features.Community.Entities;
|
||||
using Moonlight.Features.Community.Services;
|
||||
using Moonlight.Features.StoreSystem.Entities;
|
||||
using Moonlight.Features.Ticketing.Entities;
|
||||
|
||||
namespace Moonlight.Core.Services.Users;
|
||||
|
||||
public class UserDeleteService
|
||||
{
|
||||
private readonly Repository<Service> ServiceRepository;
|
||||
private readonly Repository<ServiceShare> ServiceShareRepository;
|
||||
private readonly Repository<Post> PostRepository;
|
||||
private readonly Repository<User> UserRepository;
|
||||
private readonly Repository<Transaction> TransactionRepository;
|
||||
private readonly Repository<CouponUse> CouponUseRepository;
|
||||
private readonly Repository<GiftCodeUse> GiftCodeUseRepository;
|
||||
private readonly Repository<Ticket> TicketRepository;
|
||||
private readonly Repository<TicketMessage> TicketMessageRepository;
|
||||
private readonly ServiceService ServiceService;
|
||||
private readonly PostService PostService;
|
||||
|
||||
public UserDeleteService(
|
||||
Repository<Service> serviceRepository,
|
||||
ServiceService serviceService,
|
||||
PostService postService,
|
||||
Repository<Post> postRepository,
|
||||
Repository<User> userRepository,
|
||||
Repository<GiftCodeUse> giftCodeUseRepository,
|
||||
Repository<CouponUse> couponUseRepository,
|
||||
Repository<Transaction> transactionRepository,
|
||||
Repository<Ticket> ticketRepository,
|
||||
Repository<TicketMessage> ticketMessageRepository,
|
||||
Repository<ServiceShare> serviceShareRepository)
|
||||
{
|
||||
ServiceRepository = serviceRepository;
|
||||
ServiceService = serviceService;
|
||||
PostService = postService;
|
||||
PostRepository = postRepository;
|
||||
UserRepository = userRepository;
|
||||
GiftCodeUseRepository = giftCodeUseRepository;
|
||||
CouponUseRepository = couponUseRepository;
|
||||
TransactionRepository = transactionRepository;
|
||||
TicketRepository = ticketRepository;
|
||||
TicketMessageRepository = ticketMessageRepository;
|
||||
ServiceShareRepository = serviceShareRepository;
|
||||
}
|
||||
|
||||
public async Task Perform(User user)
|
||||
{
|
||||
// Community
|
||||
|
||||
// - Posts
|
||||
foreach (var post in PostRepository.Get().ToArray())
|
||||
{
|
||||
await PostService.Delete(post);
|
||||
}
|
||||
|
||||
// - Comments
|
||||
var posts = PostRepository
|
||||
.Get()
|
||||
.Where(x => x.Comments.Any(y => y.Author.Id == user.Id))
|
||||
.ToArray();
|
||||
|
||||
foreach (var post in posts)
|
||||
{
|
||||
var comments = PostRepository
|
||||
.Get()
|
||||
.Include(x => x.Comments)
|
||||
.ThenInclude(x => x.Author)
|
||||
.First(x => x.Id == post.Id)
|
||||
.Comments
|
||||
.Where(x => x.Author.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var comment in comments)
|
||||
await PostService.DeleteComment(post, comment);
|
||||
}
|
||||
|
||||
// Services
|
||||
foreach (var service in ServiceRepository.Get().Where(x => x.Owner.Id == user.Id).ToArray())
|
||||
{
|
||||
await ServiceService.Admin.Delete(service);
|
||||
}
|
||||
|
||||
// Service shares
|
||||
var shares = ServiceShareRepository
|
||||
.Get()
|
||||
.Where(x => x.User.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var share in shares)
|
||||
{
|
||||
ServiceShareRepository.Delete(share);
|
||||
}
|
||||
|
||||
// Transactions - Coupons - Gift codes
|
||||
var userWithDetails = UserRepository
|
||||
.Get()
|
||||
.Include(x => x.Transactions)
|
||||
.Include(x => x.CouponUses)
|
||||
.Include(x => x.GiftCodeUses)
|
||||
.First(x => x.Id == user.Id);
|
||||
|
||||
var giftCodeUses = userWithDetails.GiftCodeUses.ToArray();
|
||||
var couponUses = userWithDetails.CouponUses.ToArray();
|
||||
var transactions = userWithDetails.Transactions.ToArray();
|
||||
|
||||
userWithDetails.GiftCodeUses.Clear();
|
||||
userWithDetails.CouponUses.Clear();
|
||||
userWithDetails.Transactions.Clear();
|
||||
|
||||
UserRepository.Update(userWithDetails);
|
||||
|
||||
foreach (var giftCodeUse in giftCodeUses)
|
||||
GiftCodeUseRepository.Delete(giftCodeUse);
|
||||
|
||||
foreach (var couponUse in couponUses)
|
||||
CouponUseRepository.Delete(couponUse);
|
||||
|
||||
foreach (var transaction in transactions)
|
||||
TransactionRepository.Delete(transaction);
|
||||
|
||||
// Tickets and ticket messages
|
||||
|
||||
// First we need to fetch every message this user has sent and delete it as admin accounts can have messages
|
||||
// in tickets they dont own
|
||||
var messagesFromUser = TicketMessageRepository
|
||||
.Get()
|
||||
.Where(x => x.Sender.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var message in messagesFromUser)
|
||||
{
|
||||
TicketMessageRepository.Delete(message);
|
||||
}
|
||||
|
||||
// Now we can only delete the tickets the user actually owns
|
||||
var tickets = TicketRepository
|
||||
.Get()
|
||||
.Include(x => x.Messages)
|
||||
.Where(x => x.Creator.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
var messages = ticket.Messages.ToArray(); // Cache message models
|
||||
|
||||
ticket.Messages.Clear();
|
||||
TicketRepository.Update(ticket);
|
||||
|
||||
foreach (var ticketMessage in messages)
|
||||
{
|
||||
TicketMessageRepository.Delete(ticketMessage);
|
||||
}
|
||||
|
||||
TicketRepository.Delete(ticket);
|
||||
}
|
||||
|
||||
// User
|
||||
|
||||
// We need to use this in order to entity framework not crashing because of the previous deleted data
|
||||
var userToDelete = UserRepository.Get().First(x => x.Id == user.Id);
|
||||
UserRepository.Delete(userToDelete);
|
||||
}
|
||||
}
|
||||
40
Moonlight/Core/Services/Users/UserDetailsService.cs
Normal file
40
Moonlight/Core/Services/Users/UserDetailsService.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Repositories;
|
||||
|
||||
namespace Moonlight.Core.Services.Users;
|
||||
|
||||
public class UserDetailsService
|
||||
{
|
||||
private readonly BucketService BucketService;
|
||||
private readonly Repository<User> UserRepository;
|
||||
|
||||
public UserDetailsService(BucketService bucketService, Repository<User> userRepository)
|
||||
{
|
||||
BucketService = bucketService;
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateAvatar(User user, Stream stream, string fileName)
|
||||
{
|
||||
if (user.Avatar != null)
|
||||
{
|
||||
await BucketService.Delete("avatars", user.Avatar, true);
|
||||
}
|
||||
|
||||
var file = await BucketService.Store("avatars", stream, fileName);
|
||||
|
||||
user.Avatar = file;
|
||||
UserRepository.Update(user);
|
||||
}
|
||||
|
||||
public async Task UpdateAvatar(User user) // Overload to reset avatar
|
||||
{
|
||||
if (user.Avatar != null)
|
||||
{
|
||||
await BucketService.Delete("avatars", user.Avatar, true);
|
||||
}
|
||||
|
||||
user.Avatar = null;
|
||||
UserRepository.Update(user);
|
||||
}
|
||||
}
|
||||
45
Moonlight/Core/Services/Users/UserService.cs
Normal file
45
Moonlight/Core/Services/Users/UserService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Moonlight.Core.Database.Entities;
|
||||
using Moonlight.Core.Exceptions;
|
||||
using Moonlight.Core.Repositories;
|
||||
|
||||
namespace Moonlight.Core.Services.Users;
|
||||
|
||||
public class UserService
|
||||
{
|
||||
private readonly Repository<User> UserRepository;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public UserAuthService Auth => ServiceProvider.GetRequiredService<UserAuthService>();
|
||||
public UserDetailsService Details => ServiceProvider.GetRequiredService<UserDetailsService>();
|
||||
public UserDeleteService Delete => ServiceProvider.GetRequiredService<UserDeleteService>();
|
||||
|
||||
public UserService(
|
||||
Repository<User> userRepository,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public Task Update(User user, string username, string email)
|
||||
{
|
||||
// Event though we have form validation i want to
|
||||
// ensure that at least these basic formatting things are done
|
||||
email = email.ToLower().Trim();
|
||||
username = username.ToLower().Trim();
|
||||
|
||||
// Prevent duplication or username and/or email
|
||||
if (UserRepository.Get().Any(x => x.Email == email && x.Id != user.Id))
|
||||
throw new DisplayException("A user with that email does already exist");
|
||||
|
||||
if (UserRepository.Get().Any(x => x.Username == username && x.Id != user.Id))
|
||||
throw new DisplayException("A user with that username does already exist");
|
||||
|
||||
user.Username = username;
|
||||
user.Email = email;
|
||||
|
||||
UserRepository.Update(user);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
34
Moonlight/Core/Services/Utils/ConnectionService.cs
Normal file
34
Moonlight/Core/Services/Utils/ConnectionService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Moonlight.Core.Helpers;
|
||||
|
||||
namespace Moonlight.Core.Services.Utils;
|
||||
|
||||
public class ConnectionService
|
||||
{
|
||||
private readonly IHttpContextAccessor ContextAccessor;
|
||||
private readonly ConfigService ConfigService;
|
||||
|
||||
public ConnectionService(IHttpContextAccessor contextAccessor, ConfigService configService)
|
||||
{
|
||||
ContextAccessor = contextAccessor;
|
||||
ConfigService = configService;
|
||||
}
|
||||
|
||||
public Task<string> GetIpAddress()
|
||||
{
|
||||
if (ContextAccessor.HttpContext == null)
|
||||
return Task.FromResult("N/A (Missing http context)");
|
||||
|
||||
var request = ContextAccessor.HttpContext.Request;
|
||||
|
||||
if (request.Headers.ContainsKey("X-Real-IP"))
|
||||
{
|
||||
if(ConfigService.Get().Security.EnableReverseProxyMode)
|
||||
return Task.FromResult(request.Headers["X-Real-IP"].ToString());
|
||||
|
||||
Logger.Warn($"Detected an ip mask attempt by using a fake X-Real-IP header. Fake IP: {request.Headers["X-Real-IP"]}. Real IP: {ContextAccessor.HttpContext.Connection.RemoteIpAddress}");
|
||||
return Task.FromResult(ContextAccessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "N/A (Remote IP missing)");
|
||||
}
|
||||
|
||||
return Task.FromResult(ContextAccessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "N/A (Remote IP missing)");
|
||||
}
|
||||
}
|
||||
75
Moonlight/Core/Services/Utils/JwtService.cs
Normal file
75
Moonlight/Core/Services/Utils/JwtService.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using JWT.Algorithms;
|
||||
using JWT.Builder;
|
||||
using Moonlight.Core.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Moonlight.Core.Services.Utils;
|
||||
|
||||
public class JwtService
|
||||
{
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly TimeSpan DefaultDuration = TimeSpan.FromDays(365 * 10);
|
||||
|
||||
public JwtService(ConfigService configService)
|
||||
{
|
||||
ConfigService = configService;
|
||||
}
|
||||
|
||||
public Task<string> Create(Action<Dictionary<string, string>> data, TimeSpan? validDuration = null)
|
||||
{
|
||||
var builder = new JwtBuilder()
|
||||
.WithSecret(ConfigService.Get().Security.Token)
|
||||
.IssuedAt(DateTime.UtcNow)
|
||||
.ExpirationTime(DateTime.UtcNow.Add(validDuration ?? DefaultDuration))
|
||||
.WithAlgorithm(new HMACSHA512Algorithm());
|
||||
|
||||
var dataDic = new Dictionary<string, string>();
|
||||
data.Invoke(dataDic);
|
||||
|
||||
foreach (var entry in dataDic)
|
||||
builder = builder.AddClaim(entry.Key, entry.Value);
|
||||
|
||||
var jwt = builder.Encode();
|
||||
|
||||
return Task.FromResult(jwt);
|
||||
}
|
||||
|
||||
public Task<bool> Validate(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = new JwtBuilder()
|
||||
.WithSecret(ConfigService.Get().Security.Token)
|
||||
.WithAlgorithm(new HMACSHA512Algorithm())
|
||||
.MustVerifySignature()
|
||||
.Decode(token);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn(e.Message);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Dictionary<string, string>> Decode(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = new JwtBuilder()
|
||||
.WithSecret(ConfigService.Get().Security.Token)
|
||||
.WithAlgorithm(new HMACSHA512Algorithm())
|
||||
.MustVerifySignature()
|
||||
.Decode(token);
|
||||
|
||||
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
|
||||
|
||||
return Task.FromResult(data)!;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return Task.FromResult<Dictionary<string, string>>(null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user