Added admin resource manager

This commit is contained in:
Marcel Baumgartner
2023-03-09 08:52:26 +01:00
parent f1a32c0e5b
commit dfb2a5eaab
10 changed files with 396 additions and 24 deletions

View File

@@ -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<FileManagerObject[]> GetDirectoryContent()
{
var x = new List<FileManagerObject>();
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<string> 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<int>? 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<string> GetCurrentPath()
{
return Task.FromResult(Path);
}
public Task<Stream> GetDownloadStream(FileManagerObject managerObject)
{
var stream = new FileStream(RealPath + managerObject.Name, FileMode.Open, FileAccess.Read);
return Task.FromResult<Stream>(stream);
}
public Task<string> 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<string> GetLaunchUrl()
{
throw new DisplayException("WinSCP cannot be launched here");
}
}

View File

@@ -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<string>("Server");
Password = mailConfig.GetValue<string>("Password");
Email = mailConfig.GetValue<string>("Email");
Port = mailConfig.GetValue<int>("Port");
}
public async Task SendMail(
User user,
string name,
Action<Dictionary<string, string>> 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<string, string>();
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<string, string> values)
{
foreach (var kvp in values)
{
html = html.Replace("{{" + kvp.Key + "}}", kvp.Value);
}
return html;
}
}

View File

@@ -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<string> 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<string> 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<string> GenerateToken(User user)
public async Task<string> 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;
}
}

View File

@@ -13,6 +13,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="Blazor.ContextMenu" Version="1.15.0" />
<PackageReference Include="BlazorDownloadFile" Version="2.4.0.2" />
<PackageReference Include="Blazored.Typeahead" Version="4.7.0" />
<PackageReference Include="BlazorMonaco" Version="2.1.0" />
<PackageReference Include="BlazorTable" Version="1.17.0" />

View File

@@ -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<AuditLogService>();
builder.Services.AddScoped<ErrorLogService>();
builder.Services.AddScoped<LogService>();
builder.Services.AddScoped<MailService>();
// Support
builder.Services.AddSingleton<SupportServerService>();
@@ -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();

View File

@@ -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
<div class="card card-flush">
@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());
}

View File

@@ -6,17 +6,32 @@
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/system">
Overview
<TL>Overview</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/system/logs">
Logs
<TL>Logs</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/system/auditlog">
AuditLog
<TL>AuditLog</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/admin/system/auditlog">
<TL>SecurityLog</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 4 ? "active" : "")" href="/admin/system/auditlog">
<TL>ErrorLog</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 5 ? "active" : "")" href="/admin/system/resources">
<TL>Resources</TL>
</a>
</li>
</ul>

View File

@@ -0,0 +1,14 @@
@page "/admin/system/resources"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Models.Files.Accesses
<OnlyAdmin>
<AdminSystemNavigation Index="5" />
<div class="card card-body">
<FileManager FileAccess="@(new HostFileAccess("resources"))">
</FileManager>
</div>
</OnlyAdmin>

View File

@@ -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

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>New moonlight login</title>
</head>
<body>
<div style="background-color:#ffffff; padding: 45px 0 34px 0; border-radius: 24px; margin:40px auto; max-width: 600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" height="auto"
style="border-collapse:collapse">
<tbody>
<tr>
<td align="center" valign="center" style="text-align:center; padding-bottom: 10px">
<div style="text-align:center; margin:0 15px 34px 15px">
<div style="margin-bottom: 10px">
<a href="https://endelon-hosting.de" rel="noopener" target="_blank">
<img alt="Logo" src="https://moonlight.endelon-hosting.de/assets/media/logo/MoonFullText.png" style="height: 35px">
</a>
</div>
<div style="font-size: 14px; font-weight: 500; margin-bottom: 27px; font-family:Arial,Helvetica,sans-serif;">
<p style="margin-bottom:9px; color:#181C32; font-size: 22px; font-weight:700">Hey {{FirstName}}, there is a new login in your moonlight account</p>
<p style="margin-bottom:2px; color:#7E8299">Here is all the data we collected</p>
<p style="margin-bottom:2px; color:#7E8299">IP: {{Ip}}</p>
<p style="margin-bottom:2px; color:#7E8299">Device: {{Device}}</p>
<p style="margin-bottom:2px; color:#7E8299">Location: {{Location}}</p>
</div>
<a href="https://moonlight.endelon-hosting.de" target="_blank"
style="background-color:#50cd89; border-radius:6px;display:inline-block; padding:11px 19px; color: #FFFFFF; font-size: 14px; font-weight:500;">Open Moonlight
</a>
</div>
</td>
</tr>
<tr>
<td align="center" valign="center"
style="font-size: 13px; text-align:center; padding: 0 10px 10px 10px; font-weight: 500; color: #A1A5B7; font-family:Arial,Helvetica,sans-serif">
<p style="color:#181C32; font-size: 16px; font-weight: 600; margin-bottom:9px">You need help?</p>
<p style="margin-bottom:2px">We are happy to help!</p>
<p style="margin-bottom:4px">More information at
<a href="https://endelon.link/support" rel="noopener" target="_blank" style="font-weight: 600">endelon.link/support</a>.
</p>
</td>
</tr>
<tr>
<td align="center" valign="center"
style="font-size: 13px; padding:0 15px; text-align:center; font-weight: 500; color: #A1A5B7;font-family:Arial,Helvetica,sans-serif">
<p>Copyright 2022 Endelon Hosting </p>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>