From dfb2a5eaabe471168fd3322b89a1d6aaad0b3e31 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Thu, 9 Mar 2023 08:52:26 +0100 Subject: [PATCH] Added admin resource manager --- .../Models/Files/Accesses/HostFileAccess.cs | 145 ++++++++++++++++++ Moonlight/App/Services/MailService.cs | 90 +++++++++++ Moonlight/App/Services/UserService.cs | 44 ++++-- Moonlight/Moonlight.csproj | 1 + Moonlight/Program.cs | 8 +- .../FileManagerPartials/FileManager.razor | 28 +++- .../Navigations/AdminSystemNavigation.razor | 21 ++- .../Shared/Views/Admin/Sys/Resources.razor | 14 ++ Moonlight/resources/lang/de_de.lang | 16 ++ Moonlight/resources/mail/login.html | 53 +++++++ 10 files changed, 396 insertions(+), 24 deletions(-) create mode 100644 Moonlight/App/Models/Files/Accesses/HostFileAccess.cs create mode 100644 Moonlight/App/Services/MailService.cs create mode 100644 Moonlight/Shared/Views/Admin/Sys/Resources.razor create mode 100644 Moonlight/resources/mail/login.html diff --git a/Moonlight/App/Models/Files/Accesses/HostFileAccess.cs b/Moonlight/App/Models/Files/Accesses/HostFileAccess.cs new file mode 100644 index 00000000..ef0a66ee --- /dev/null +++ b/Moonlight/App/Models/Files/Accesses/HostFileAccess.cs @@ -0,0 +1,145 @@ +using Logging.Net; +using Moonlight.App.Exceptions; +using Moonlight.App.Helpers; + +namespace Moonlight.App.Models.Files.Accesses; + +public class HostFileAccess : IFileAccess +{ + private readonly string BasePath; + private string RealPath => BasePath + Path; + private string Path = "/"; + + public HostFileAccess(string bp) + { + BasePath = bp; + } + + public Task GetDirectoryContent() + { + var x = new List(); + + foreach (var directory in Directory.EnumerateDirectories(RealPath)) + { + x.Add(new () + { + Name = System.IO.Path.GetFileName(directory)!, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Size = 0, + IsFile = false + }); + } + + foreach (var file in Directory.GetFiles(RealPath)) + { + x.Add(new () + { + Name = System.IO.Path.GetFileName(file)!, + CreatedAt = File.GetCreationTimeUtc(file), + UpdatedAt = File.GetLastWriteTimeUtc(file), + Size = new System.IO.FileInfo(file).Length, + IsFile = File.Exists(file) + }); + } + + return Task.FromResult(x.ToArray()); + } + + public Task ChangeDirectory(string s) + { + var x = System.IO.Path.Combine(Path, s).Replace("\\", "/") + "/"; + x = x.Replace("//", "/"); + Path = x; + + return Task.CompletedTask; + } + + public Task SetDirectory(string s) + { + Path = s; + return Task.CompletedTask; + } + + public Task GoUp() + { + Path = System.IO.Path.GetFullPath(System.IO.Path.Combine(Path, "..")).Replace("\\", "/").Replace("C:", ""); + return Task.CompletedTask; + } + + public Task ReadFile(FileManagerObject fileManagerObject) + { + return Task.FromResult(File.ReadAllText(RealPath + fileManagerObject.Name)); + } + + public Task WriteFile(FileManagerObject fileManagerObject, string content) + { + File.WriteAllText(RealPath + fileManagerObject.Name, content); + return Task.CompletedTask; + } + + public async Task UploadFile(string name, Stream stream, Action? progressUpdated = null) + { + var fs = File.OpenWrite(RealPath + name); + + var dataStream = new StreamProgressHelper(stream) + { + Progress = i => + { + if (progressUpdated != null) + progressUpdated.Invoke(i); + } + }; + + await dataStream.CopyToAsync(fs); + await fs.FlushAsync(); + fs.Close(); + } + + public Task CreateDirectory(string name) + { + Directory.CreateDirectory(RealPath + name); + return Task.CompletedTask; + } + + public Task GetCurrentPath() + { + return Task.FromResult(Path); + } + + public Task GetDownloadStream(FileManagerObject managerObject) + { + var stream = new FileStream(RealPath + managerObject.Name, FileMode.Open, FileAccess.Read); + return Task.FromResult(stream); + } + + public Task GetDownloadUrl(FileManagerObject managerObject) + { + throw new NotImplementedException(); + } + + public Task Delete(FileManagerObject managerObject) + { + if(managerObject.IsFile) + File.Delete(RealPath + managerObject.Name); + else + Directory.Delete(RealPath + managerObject.Name, true); + + return Task.CompletedTask; + } + + public Task Move(FileManagerObject managerObject, string newPath) + { + if(managerObject.IsFile) + File.Move(RealPath + managerObject.Name, BasePath + newPath); + else + Directory.Move(RealPath + managerObject.Name, BasePath + newPath); + + return Task.CompletedTask; + } + + public Task GetLaunchUrl() + { + throw new DisplayException("WinSCP cannot be launched here"); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/MailService.cs b/Moonlight/App/Services/MailService.cs new file mode 100644 index 00000000..24aa35dd --- /dev/null +++ b/Moonlight/App/Services/MailService.cs @@ -0,0 +1,90 @@ +using System.Net; +using System.Net.Mail; +using Logging.Net; +using Moonlight.App.Database.Entities; +using Moonlight.App.Exceptions; + +namespace Moonlight.App.Services; + +public class MailService +{ + private readonly string Server; + private readonly string Password; + private readonly string Email; + private readonly int Port; + + public MailService(ConfigService configService) + { + var mailConfig = configService + .GetSection("Moonlight") + .GetSection("Mail"); + + Server = mailConfig.GetValue("Server"); + Password = mailConfig.GetValue("Password"); + Email = mailConfig.GetValue("Email"); + Port = mailConfig.GetValue("Port"); + } + + public async Task SendMail( + User user, + string name, + Action> values + ) + { + if (!File.Exists($"resources/mail/{name}.html")) + { + Logger.Warn($"Mail template '{name}' not found. Make sure to place one in the resources folder"); + throw new DisplayException("Mail template not found"); + } + + var rawHtml = await File.ReadAllTextAsync($"resources/mail/{name}.html"); + + var val = new Dictionary(); + values.Invoke(val); + + val.Add("FirstName", user.FirstName); + val.Add("LastName", user.LastName); + + var parsed = ParseMail(rawHtml, val); + + Task.Run(async () => + { + try + { + using var client = new SmtpClient(); + + client.Host = Server; + client.Port = Port; + client.EnableSsl = true; + client.Credentials = new NetworkCredential(Email, Password); + + await client.SendMailAsync(new MailMessage() + { + From = new MailAddress(Email), + Sender = new MailAddress(Email), + Body = parsed, + IsBodyHtml = true, + Subject = $"Hey {user.FirstName}, there are news from moonlight", + To = { new MailAddress(user.Email) } + }); + + Logger.Debug("Send!"); + } + catch (Exception e) + { + Logger.Warn("Error sending mail"); + Logger.Warn(e); + } + }); + } + + private string ParseMail(string html, Dictionary values) + { + foreach (var kvp in values) + { + html = html.Replace("{{" + kvp.Key + "}}", kvp.Value); + } + + return html; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/UserService.cs b/Moonlight/App/Services/UserService.cs index 0de40d8e..196de75b 100644 --- a/Moonlight/App/Services/UserService.cs +++ b/Moonlight/App/Services/UserService.cs @@ -5,6 +5,7 @@ using Moonlight.App.Exceptions; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Services.LogServices; +using Moonlight.App.Services.Sessions; namespace Moonlight.App.Services; @@ -14,6 +15,8 @@ public class UserService private readonly TotpService TotpService; private readonly SecurityLogService SecurityLogService; private readonly AuditLogService AuditLogService; + private readonly MailService MailService; + private readonly IdentityService IdentityService; private readonly string JwtSecret; @@ -22,12 +25,16 @@ public class UserService TotpService totpService, ConfigService configService, SecurityLogService securityLogService, - AuditLogService auditLogService) + AuditLogService auditLogService, + MailService mailService, + IdentityService identityService) { UserRepository = userRepository; TotpService = totpService; SecurityLogService = securityLogService; AuditLogService = auditLogService; + MailService = mailService; + IdentityService = identityService; JwtSecret = configService .GetSection("Moonlight") @@ -37,14 +44,17 @@ public class UserService public async Task Register(string email, string password, string firstname, string lastname) { + // Check if the email is already taken var emailTaken = UserRepository.Get().FirstOrDefault(x => x.Email == email) != null; if (emailTaken) { - //AuditLogService.Log("register:fail", $"Invalid email: {email}"); throw new DisplayException("The email is already in use"); } + + //TODO: Validation + // Add user var user = UserRepository.Add(new() { Address = "", @@ -67,11 +77,7 @@ public class UserService TokenValidTime = DateTime.Now.AddDays(-5) }); - //AuditLogService.Log("register:done", $"A new user has registered: Email: {email}"); - - //var mail = new WelcomeMail(user); - //await MailService.Send(mail, user); - + await MailService.SendMail(user!, "register", values => {}); await AuditLogService.Log(AuditLogType.Register, user.Email); return await GenerateToken(user); @@ -101,6 +107,7 @@ public class UserService public async Task Login(string email, string password, string totpCode = "") { + // First password check and check if totp is enabled var needTotp = await CheckTotp(email, password); var user = UserRepository.Get() @@ -120,7 +127,7 @@ public class UserService if (totpCodeValid) { await AuditLogService.Log(AuditLogType.Login, email); - return await GenerateToken(user); + return await GenerateToken(user, true); } else { @@ -131,7 +138,7 @@ public class UserService else { await AuditLogService.Log(AuditLogType.Login, email); - return await GenerateToken(user!); + return await GenerateToken(user!, true); } } @@ -141,8 +148,12 @@ public class UserService user.TokenValidTime = DateTime.Now; UserRepository.Update(user); - //var mail = new NewPasswordMail(user); - //await MailService.Send(mail, user); + await MailService.SendMail(user!, "passwordChange", values => + { + values.Add("Ip", IdentityService.GetIp()); + values.Add("Device", IdentityService.GetDevice()); + values.Add("Location", "In your walls"); + }); await AuditLogService.Log(AuditLogType.ChangePassword, user.Email); } @@ -167,8 +178,15 @@ public class UserService throw new Exception("Invalid userid or password"); } - public Task GenerateToken(User user) + public async Task GenerateToken(User user, bool sendMail = false) { + await MailService.SendMail(user!, "login", values => + { + values.Add("Ip", IdentityService.GetIp()); + values.Add("Device", IdentityService.GetDevice()); + values.Add("Location", "In your walls"); + }); + var token = JwtBuilder.Create() .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(JwtSecret) @@ -177,6 +195,6 @@ public class UserService .AddClaim("userid", user.Id) .Encode(); - return Task.FromResult(token); + return token; } } \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 55d6a86f..c9c7eeda 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -13,6 +13,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 83a37f78..36dce008 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,3 +1,4 @@ +using BlazorDownloadFile; using BlazorTable; using CurrieTechnologies.Razor.SweetAlert2; using Logging.Net; @@ -34,8 +35,9 @@ namespace Moonlight var builder = WebApplication.CreateBuilder(args); // Switch to logging.net injection - builder.Logging.ClearProviders(); - builder.Logging.AddProvider(new LogMigratorProvider()); + // TODO: Enable in production + //builder.Logging.ClearProviders(); + //builder.Logging.AddProvider(new LogMigratorProvider()); // Add services to the container. builder.Services.AddRazorPages(); @@ -97,6 +99,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Support builder.Services.AddSingleton(); @@ -120,6 +123,7 @@ namespace Moonlight builder.Services.AddBlazorTable(); builder.Services.AddSweetAlert2(options => { options.Theme = SweetAlertTheme.Dark; }); builder.Services.AddBlazorContextMenu(); + builder.Services.AddBlazorDownloadFile(); var app = builder.Build(); diff --git a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor index f89bc024..cf8c13b6 100644 --- a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor +++ b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor @@ -1,5 +1,6 @@ @using Moonlight.App.Helpers @using BlazorContextMenu +@using BlazorDownloadFile @using Logging.Net @using Moonlight.App.Models.Files @using Moonlight.App.Services @@ -9,6 +10,7 @@ @inject ToastService ToastService @inject NavigationManager NavigationManager @inject SmartTranslateService TranslationService +@inject IBlazorDownloadFileService FileService
@if (Editing == null) @@ -347,18 +349,32 @@ else case "download": if (data.IsFile) { - // First we try to download via stream - try { - var url = await FileAccess.GetDownloadUrl(data); - NavigationManager.NavigateTo(url, true); + var stream = await FileAccess.GetDownloadStream(data); await ToastService.Info(TranslationService.Translate("Starting download")); + await FileService.AddBuffer(stream); + await FileService.DownloadBinaryBuffers(data.Name); + } + catch (NotImplementedException) + { + try + { + var url = await FileAccess.GetDownloadUrl(data); + NavigationManager.NavigateTo(url, true); + await ToastService.Info(TranslationService.Translate("Starting download")); + } + catch (Exception exception) + { + await ToastService.Error(TranslationService.Translate("Error starting download")); + Logger.Error("Error downloading file"); + Logger.Error(exception.Message); + } } catch (Exception exception) { await ToastService.Error(TranslationService.Translate("Error starting download")); - Logger.Error("Error downloading file"); + Logger.Error("Error downloading file stream"); Logger.Error(exception.Message); } } @@ -462,7 +478,7 @@ else await InvokeAsync(StateHasChanged); } - private async void Launch() + private async Task Launch() { NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl()); } diff --git a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor index 0d0af9f0..fa2c2a9a 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -6,17 +6,32 @@ diff --git a/Moonlight/Shared/Views/Admin/Sys/Resources.razor b/Moonlight/Shared/Views/Admin/Sys/Resources.razor new file mode 100644 index 00000000..cbb4fd54 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Sys/Resources.razor @@ -0,0 +1,14 @@ +@page "/admin/system/resources" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.Shared.Components.FileManagerPartials +@using Moonlight.App.Models.Files.Accesses + + + + +
+ + +
+
\ No newline at end of file diff --git a/Moonlight/resources/lang/de_de.lang b/Moonlight/resources/lang/de_de.lang index 20e451e6..b470c8cd 100644 --- a/Moonlight/resources/lang/de_de.lang +++ b/Moonlight/resources/lang/de_de.lang @@ -290,3 +290,19 @@ Enter the message to send;Enter the message to send Confirm;Confirm Are you sure?;Are you sure? Enter url;Enter url +An unknown error occured while starting backup deletion;An unknown error occured while starting backup deletion +Success;Success +Backup URL successfully copied to your clipboard;Backup URL successfully copied to your clipboard +Backup restore started;Backup restore started +Backup successfully restored;Backup successfully restored +Register for;Register for +Core;Core +Logs;Logs +AuditLog;AuditLog +SecurityLog;SecurityLog +ErrorLog;ErrorLog +Resources;Resources +WinSCP cannot be launched here;WinSCP cannot be launched here +Create a new folder;Create a new folder +Enter a name;Enter a name +File upload complete;File upload complete diff --git a/Moonlight/resources/mail/login.html b/Moonlight/resources/mail/login.html new file mode 100644 index 00000000..06748d96 --- /dev/null +++ b/Moonlight/resources/mail/login.html @@ -0,0 +1,53 @@ + + + + + New moonlight login + + +
+ + + + + + + + + + + + +
+
+
+ + Logo + +
+
+

Hey {{FirstName}}, there is a new login in your moonlight account

+

Here is all the data we collected

+

IP: {{Ip}}

+

Device: {{Device}}

+

Location: {{Location}}

+
+ Open Moonlight + +
+
+

You need help?

+

We are happy to help!

+

More information at + endelon.link/support. +

+
+

Copyright 2022 Endelon Hosting

+
+
+ + \ No newline at end of file