14 Commits
v1b9 ... v1b10

Author SHA1 Message Date
Marcel Baumgartner
a8bd1193ce Merge pull request #195 from Moonlight-Panel/SecurityPatches
Security patches
2023-06-26 00:10:03 +02:00
Marcel Baumgartner
366d1a9205 Merge pull request #194 from Moonlight-Panel/AddStreamerModeAndFixUI
Added streamer mode and fixed security settings ui
2023-06-26 00:07:23 +02:00
Marcel Baumgartner
df9ed95c6b Added streamer mode and fixed security settings ui 2023-06-26 00:06:44 +02:00
Marcel Baumgartner
23a211362e Merge pull request #193 from Moonlight-Panel/EnhanceFileManager
Enhanced winscp button and new file/folder menu
2023-06-25 17:32:57 +02:00
Marcel Baumgartner
cf91d44902 Enhanced winscp button and new file/folder menu 2023-06-25 17:31:36 +02:00
Marcel Baumgartner
35633e21a9 Merge pull request #192 from Moonlight-Panel/EnhanceServerListLayout
Enhanced server list
2023-06-25 00:01:53 +02:00
Marcel Baumgartner
ce8b8f6798 Enhanced server list 2023-06-25 00:01:28 +02:00
Marcel Baumgartner
c28c80ba25 Merge pull request #191 from Moonlight-Panel/FixDnsManager
Fixed dns loading issues. Added udp support
2023-06-24 23:45:49 +02:00
Marcel Baumgartner
da17b1df93 Fixed dns loading issues. Added udp support 2023-06-24 23:45:29 +02:00
Marcel Baumgartner
f9f5865ef9 Prevent user locking when duplicating the email entries 2023-06-24 22:35:38 +02:00
Marcel Baumgartner
389ded9b77 Fixed oauth2 account spoofing using unverified discord accounts for claiming identity 2023-06-24 22:15:04 +02:00
Marcel Baumgartner
faebaa59dd Merge pull request #189 from Moonlight-Panel/AddCustomLayoutServerList
Added custom layout options for the server list
2023-06-24 02:35:21 +02:00
Marcel Baumgartner
6b7dc2ad05 Added custom layout options for the server list 2023-06-24 02:35:01 +02:00
Marcel Baumgartner
e356c9d0c8 Update README.md 2023-06-23 05:06:12 +02:00
24 changed files with 3088 additions and 407 deletions

View File

@@ -24,6 +24,8 @@ public class User
public string Country { get; set; } = "";
public string ServerListLayoutJson { get; set; } = "";
// States
public UserStatus Status { get; set; } = UserStatus.Unverified;
@@ -31,6 +33,7 @@ public class User
public bool SupportPending { get; set; } = false;
public bool HasRated { get; set; } = false;
public int Rating { get; set; } = 0;
public bool StreamerMode { get; set; } = false;
// Security
public bool TotpEnabled { get; set; } = false;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerListLayoutToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ServerListLayoutJson",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ServerListLayoutJson",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedStreamerMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "StreamerMode",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StreamerMode",
table: "Users");
}
}
}

View File

@@ -780,6 +780,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<string>("ServerListLayoutJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("State")
.IsRequired()
.HasColumnType("longtext");
@@ -787,6 +791,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Status")
.HasColumnType("int");
b.Property<bool>("StreamerMode")
.HasColumnType("tinyint(1)");
b.Property<int>("SubscriptionDuration")
.HasColumnType("int");

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.Models.Forms;
public class UserPreferencesDataModel
{
public bool StreamerMode { get; set; } = false;
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Misc;
public class ServerGroup
{
public string Name { get; set; } = "";
public List<string> Servers { get; set; } = new();
}

View File

@@ -86,6 +86,13 @@ public class DiscordOAuth2Provider : OAuth2Provider
var email = getData.GetValue<string>("email");
var id = getData.GetValue<ulong>("id");
var verified = getData.GetValue<bool>("verified");
if (!verified)
{
Logger.Warn("A user tried to use an unverified discord account to login", "security");
throw new DisplayException("You can only use verified discord accounts for oauth signin");
}
// Handle data

View File

@@ -1,5 +1,6 @@
using CloudFlare.Client;
using CloudFlare.Client.Api.Authentication;
using CloudFlare.Client.Api.Display;
using CloudFlare.Client.Api.Parameters.Data;
using CloudFlare.Client.Api.Result;
using CloudFlare.Client.Api.Zones;
@@ -12,6 +13,7 @@ using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.Domains;
using DnsRecord = Moonlight.App.Models.Misc.DnsRecord;
using MatchType = CloudFlare.Client.Enumerators.MatchType;
namespace Moonlight.App.Services;
@@ -93,9 +95,36 @@ public class DomainService
{
var domain = EnsureData(d);
var records = GetData(
await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId)
);
var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>();
// Load paginated
// TODO: Find an alternative option. This way to load the records is NOT optimal
// and can result in long loading time when there are many dns records.
// As cloudflare does not offer a way to search dns records which starts
// with a specific string we are not able to filter it using the api (client)
var initialResponse = await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId);
records.AddRange(GetData(initialResponse));
// Check if there are more pages
while (initialResponse.ResultInfo.Page < initialResponse.ResultInfo.TotalPage)
{
// Get the next page of data
var nextPageResponse = await Client.Zones.DnsRecords.GetAsync(
domain.SharedDomain.CloudflareId,
displayOptions: new()
{
Page = initialResponse.ResultInfo.Page + 1
}
);
var nextPageRecords = GetData(nextPageResponse);
// Append the records from the next page to the existing records
records.AddRange(nextPageRecords);
// Update the initial response to the next page response
initialResponse = nextPageResponse;
}
var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
var dname = $".{rname}";
@@ -145,7 +174,11 @@ public class DomainService
if (dnsRecord.Type == DnsRecordType.Srv)
{
var parts = dnsRecord.Name.Split(".");
Enum.TryParse(parts[1], out Protocol protocol);
Protocol protocol = Protocol.Tcp;
if (parts[1].Contains("udp"))
protocol = Protocol.Udp;
var valueParts = dnsRecord.Content.Split(" ");

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Repositories;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions;
using OtpNet;
@@ -38,21 +39,24 @@ public class TotpService
return user!.TotpSecret;
}
public async Task Enable()
public async Task GenerateSecret()
{
var user = (await IdentityService.Get())!;
user.TotpSecret = GenerateSecret();
user.TotpSecret = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));;
UserRepository.Update(user);
//TODO: AuditLog
}
public async Task EnforceTotpLogin()
public async Task Enable(string code)
{
var user = (await IdentityService.Get())!;
if (!await Verify(user.TotpSecret, code))
{
throw new DisplayException("The 2fa code you entered is invalid");
}
user.TotpEnabled = true;
UserRepository.Update(user);
}
@@ -62,14 +66,10 @@ public class TotpService
var user = (await IdentityService.Get())!;
user.TotpEnabled = false;
user.TotpSecret = "";
UserRepository.Update(user);
//TODO: AuditLog
}
private string GenerateSecret()
{
return Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
}
}

View File

@@ -103,6 +103,8 @@
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@@shopify/draggable@1.0.0-beta.11/lib/draggable.bundle.js"></script>
<script src="https://www.google.com/recaptcha/api.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>

View File

@@ -96,7 +96,7 @@
{
<SmartForm Model="TotpData" OnValidSubmit="DoLogin">
<div class="fv-row mb-8 fv-plugins-icon-container">
<InputText @bind-Value="TotpData.Code" type="number" class="form-control bg-transparent"></InputText>
<InputText @bind-Value="TotpData.Code" type="number" class="form-control bg-transparent" placeholder="@(SmartTranslateService.Translate("2fa code"))"></InputText>
</div>
<div class="d-grid mb-10">
<button type="submit" class="btn btn-primary">

View File

@@ -0,0 +1,78 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services.Interop
@inject ModalService ModalService
<div class="modal fade" id="connectionDetails" tabindex="-1" aria-labelledby="connectionDetails" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<TL>Connection details</TL>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Host</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Host)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Port</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Port)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Username</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Username)">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<TL>Close</TL>
</button>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
private string Host = "";
private string Username = "";
private int Port;
protected override async Task OnParametersSetAsync()
{
Uri uri = new Uri(await Access.GetLaunchUrl());
Host = uri.Host;
Port = uri.Port;
Username = uri.UserInfo.Split(':')[0];
}
public async Task Show()
{
await ModalService.Show("connectionDetails");
}
}

View File

@@ -26,7 +26,7 @@ else
<div class="card-header border-0 my-2">
<div class="card-title">
<div class="d-flex flex-stack">
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged" />
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged"/>
</div>
</div>
<div class="card-toolbar">
@@ -34,7 +34,9 @@ else
@if (View != null && View.SelectedFiles.Any())
{
<div class="fw-bold me-5">
<span class="me-2">@(View.SelectedFiles.Length) <TL>selected</TL></span>
<span class="me-2">
@(View.SelectedFiles.Length) <TL>selected</TL>
</span>
</div>
<WButton Text="@(SmartTranslateService.Translate("Move"))"
@@ -57,37 +59,46 @@ else
}
else
{
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M5 16C3.3 16 2 14.7 2 13C2 11.3 3.3 10 5 10H5.1C5 9.7 5 9.3 5 9C5 6.2 7.2 4 10 4C11.9 4 13.5 5 14.3 6.5C14.8 6.2 15.4 6 16 6C17.7 6 19 7.3 19 9C19 9.4 18.9 9.7 18.8 10C18.9 10 18.9 10 19 10C20.7 10 22 11.3 22 13C22 14.7 20.7 16 19 16H5ZM8 13.6H16L12.7 10.3C12.3 9.89999 11.7 9.89999 11.3 10.3L8 13.6Z" fill="currentColor"/>
<path d="M11 13.6V19C11 19.6 11.4 20 12 20C12.6 20 13 19.6 13 19V13.6H11Z" fill="currentColor"/>
</svg>
</span>
<TL>Launch WinSCP</TL>
</button>
<div class="btn-group me-3">
<button type="button" @onclick="Launch" class="btn btn-light-primary">
<TL>Launch WinSCP</TL>
</button>
<button type="button" class="btn btn-light-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item btn" target="_blank" href="https://winscp.net/eng/downloads.php">
<TL>Download WinSCP</TL>
</a>
</li>
<li>
<button class="dropdown-item btn" @onclick="() => ConnectionDetailsModal.Show()">
<TL>Show connection details</TL>
</button>
</li>
</ul>
</div>
<button type="button" @onclick="CreateFile" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M6 22h12a2 2 0 0 0 2-2V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2zm7-18 5 5h-5V4zM8 14h3v-3h2v3h3v2h-3v3h-2v-3H8v-2z"></path>
</svg>
</span>
<TL>New file</TL>
</button>
<div class="btn-group me-3">
<button type="button" class="btn btn-light-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<TL>New</TL>&nbsp;
</button>
<ul class="dropdown-menu">
<li>
<button @onclick="CreateFile" class="dropdown-item btn">
<TL>New file</TL>
</button>
</li>
<li>
<button @onclick="CreateFolder" class="dropdown-item btn">
<TL>New folder</TL>
</button>
</li>
</ul>
</div>
<button type="button" @onclick="CreateFolder" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.2C9.7 3 10.2 3.20001 10.4 3.60001ZM16 12H13V9C13 8.4 12.6 8 12 8C11.4 8 11 8.4 11 9V12H8C7.4 12 7 12.4 7 13C7 13.6 7.4 14 8 14H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 14H8C7.4 14 7 13.6 7 13C7 12.4 7.4 12 8 12H11V14ZM16 12H13V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
</svg>
</span>
<TL>New folder</TL>
</button>
<FileUpload Access="Access" OnUploadComplete="OnComponentStateChanged" />
<FileUpload Access="Access" OnUploadComplete="OnComponentStateChanged"/>
}
</div>
</div>
@@ -110,6 +121,8 @@ else
Access="MoveAccess"
OnSubmit="OnFileMoveSubmit">
</FileSelectModal>
<ConnectionDetailsModal @ref="ConnectionDetailsModal" Access="Access"/>
}
@code
@@ -135,6 +148,9 @@ else
// Config
private ContextAction[] Actions = Array.Empty<ContextAction>();
// Connection details
private ConnectionDetailsModal ConnectionDetailsModal;
protected override void OnInitialized()
{
MoveAccess = (FileAccess)Access.Clone();
@@ -151,7 +167,7 @@ else
SmartTranslateService.Translate("Rename"),
SmartTranslateService.Translate("Enter a new name"),
x.Name
);
);
if (name != x.Name)
{
@@ -162,7 +178,7 @@ else
}
});
actions.Add(new ()
actions.Add(new()
{
Id = "download",
Name = "Download",
@@ -200,7 +216,7 @@ else
}
});
actions.Add(new ()
actions.Add(new()
{
Id = "decompress",
Name = "Decompress",
@@ -305,7 +321,7 @@ else
if (string.IsNullOrEmpty(name))
return;
await Access.Write(new FileData{IsFile = true, Name = name}, "");
await Access.Write(new FileData { IsFile = true, Name = name }, "");
await View!.Refresh();
}

View File

@@ -8,7 +8,7 @@
<div class="d-flex justify-content-between align-items-start flex-wrap mb-2">
<div class="d-flex flex-column">
<div class="d-flex align-items-center mb-2">
<a class="text-gray-900 fs-2 fw-bold me-1">@(User.FirstName) @(User.LastName)</a>
<a class="text-gray-900 fs-2 fw-bold me-1 @(User.StreamerMode ? "blur" : "")">@(User.FirstName) @(User.LastName)</a>
@if (User.Status == UserStatus.Verified)
{
@@ -16,7 +16,7 @@
}
</div>
<div class="d-flex flex-wrap fw-semibold fs-6 mb-4 pe-2">
<span class="d-flex align-items-center text-gray-400 mb-2">
<span class="d-flex align-items-center text-gray-400 mb-2 @(User.StreamerMode ? "blur" : "")">
@(User.Email)
</span>
</div>

View File

@@ -69,14 +69,16 @@
</div>
<div class="d-flex flex-column">
<div class="fw-bold d-flex align-items-center fs-5">
@(User.FirstName) @(User.LastName)
<div class="@(User.StreamerMode ? "blur" : "")">
@(User.FirstName) @(User.LastName)
</div>
@if (User.Admin)
{
<span class="badge badge-light-success fw-bold fs-8 px-2 py-1 ms-2">Admin</span>
}
</div>
<a class="fw-semibold text-muted text-hover-primary fs-7">@(User.Email)</a>
<a class="fw-semibold text-muted text-hover-primary fs-7 @(User.StreamerMode ? "blur" : "")">@(User.Email)</a>
</div>
</div>
</div>

View File

@@ -5,6 +5,8 @@
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@using Mappy.Net
@using Moonlight.App.Exceptions
@using Moonlight.App.Helpers
@inject UserRepository UserRepository
@@ -13,7 +15,7 @@
<LazyLoader Load="Load">
<SmartForm OnValidSubmit="Save" Model="Model">
<div class="card mb-5 mb-xl-10">
<div class="card-body p-9">
<div class="card-body p-9 @(CurrentUser.StreamerMode ? "blur" : "")">
<div class="row">
<div class="col-lg-6 fv-row fv-plugins-icon-container">
<div class="mb-3">
@@ -74,7 +76,7 @@
@code
{
private UserDataModel Model = new UserDataModel();
private UserDataModel Model = new();
[CascadingParameter]
public User CurrentUser { get; set; }
@@ -89,9 +91,20 @@
private Task Save()
{
CurrentUser = Mapper.Map(CurrentUser, Model);
// Prevent users from locking out other users by changing their email
CurrentUser.Email = CurrentUser.Email.ToLower();
Model.Email = Model.Email.ToLower();
var userWithThatEmail = UserRepository
.Get()
.FirstOrDefault(x => x.Email == Model.Email);
if (userWithThatEmail != null && CurrentUser.Id != userWithThatEmail.Id)
{
Logger.Warn($"A user tried to lock another user out by changing the email. Email: {Model.Email}", "security");
throw new DisplayException("A user with that email does already exist");
}
CurrentUser = Mapper.Map(CurrentUser, Model);
UserRepository.Update(CurrentUser);

View File

@@ -2,317 +2,232 @@
@using Moonlight.Shared.Components.Navigations
@using QRCoder
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using System.Text.RegularExpressions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Mappy.Net
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@inject SmartTranslateService SmartTranslateService
@inject TotpService TotpService
@inject NavigationManager NavigationManager
@inject IdentityService IdentityService
@inject UserService UserService
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject ModalService ModalService
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="2"/>
<div class="card mb-5 mb-xl-10">
<LazyLoader Load="Load">
@if (TotpEnabled)
{
<div class="alert alert-primary d-flex rounded ms-6 me-6 mt-6 mb-8">
<table class="w-100">
<tr>
<td rowspan="2">
<span class="svg-icon svg-icon-2tx svg-icon-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-100">
<h4 class="text-gray-900 fw-bold ms-4">
<TL>Your account is secured with 2fa</TL>
</h4>
</td>
<td rowspan="2">
<a @onclick="Disable" class="btn btn-primary px-6 align-self-center text-nowrap" data-bs-toggle="modal" data-bs-target="#twofactorauth">
<TL>Disable</TL>
</a>
</td>
</tr>
<tr>
<td>
<div class="fs-6 text-gray-700 pe-7 ms-4">
<TL>anyone write a fancy text here?</TL>
</div>
</td>
</tr>
</table>
<div class="row">
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Two factor authentication</TL>
</div>
</div>
}
else
{
<div class="alert alert-primary d-flex rounded ms-6 me-6 mt-6 mb-8">
<table class="w-100">
<tr>
<td rowspan="2">
<span class="svg-icon svg-icon-2tx svg-icon-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-100">
<h4 class="text-gray-900 fw-bold ms-4">
<TL>Secure your account</TL>
</h4>
</td>
<td rowspan="2">
<a @onclick="Enable" class="btn btn-primary px-6 align-self-center text-nowrap" data-bs-toggle="modal" data-bs-target="#twofactorauth">
<TL>Enable</TL>
</a>
</td>
</tr>
<tr>
<td>
<div class="fs-6 text-gray-700 pe-7 ms-4">
<TL>2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.</TL>
</div>
</td>
</tr>
</table>
<div class="card-body fs-6">
<p>
<TL>2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.</TL>
</p>
<div class="d-flex justify-content-center">
@if (User.TotpEnabled)
{
<WButton Text="@(SmartTranslateService.Translate("Disable"))"
WorkingText=""
CssClasses="btn-danger"
OnClick="DisableTwoFactor">
</WButton>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="StartTwoFactorWizard">
</WButton>
}
</div>
</div>
}
<div class="modal fade" id="twofactorauth" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered mw-650px">
<div class="modal-content">
<div class="modal-header flex-stack py-6">
<h2 class="ms-3">
<TL>Activate 2fa</TL>
</h2>
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
<span class="svg-icon svg-icon-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="6" y="17.3137" width="16" height="2" rx="1" transform="rotate(-45 6 17.3137)" fill="currentColor"></rect>
<rect x="7.41422" y="6" width="16" height="2" rx="1" transform="rotate(45 7.41422 6)" fill="currentColor"></rect>
</svg>
</span>
</div>
</div>
<div class="modal-body scroll-y ps-10 pe-10 pb-10">
<div>
<h3 class="text-dark fw-bold mb-3 mt-2">
<TL>2fa apps</TL>
</h3>
<div class="text-gray-500 fw-semibold fs-6 mb-10">
<TL>Use an app like </TL>
<a href="https://support.google.com/accounts/answer/1066447?hl=en" target="_blank">Google Authenticator</a>,
<a href="https://www.microsoft.com/en-us/account/authenticator" target="_blank">Microsoft Authenticator</a>,
<a href="https://authy.com/download/" target="_blank">Authy</a>, <TL>or</TL>
<a href="https://support.1password.com/one-time-passwords/" target="_blank">1Password</a> <TL>and scan the following QR Code</TL>
@if (EnablingTotp)
{
<div class="pt-5 text-center">
@{
QRCodeGenerator qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode
(
$"otpauth://totp/{Uri.EscapeDataString(User.Email)}?secret={TotpSecret}&issuer={Uri.EscapeDataString(Issuer)}",
QRCodeGenerator.ECCLevel.Q
);
PngByteQRCode qrCode = new PngByteQRCode(qrCodeData);
byte[] qrCodeAsPngByteArr = qrCode.GetGraphic(20);
var base64 = Convert.ToBase64String(qrCodeAsPngByteArr);
}
<img src="data:image/png;base64,@(base64)" alt="" class="mw-200px mt-2">
</div>
}
</div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed mb-8 p-6">
<span class="svg-icon svg-icon-2tx svg-icon-warning me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.3" x="2" y="2" width="20" height="20" rx="10" fill="currentColor"></rect>
<rect x="11" y="14" width="7" height="2" rx="1" transform="rotate(-90 11 14)" fill="currentColor"></rect>
<rect x="11" y="17" width="2" height="2" rx="1" transform="rotate(-90 11 17)" fill="currentColor"></rect>
</svg>
</span>
<div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold">
<div class="fs-6 text-gray-700">
<TL>If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:</TL>
<div class="fw-bold text-dark pt-2">@(TotpSecret)</div>
</div>
</div>
</div>
</div>
<a class="btn btn-primary px-6 align-self-center text-nowrap float-end" data-bs-toggle="modal" data-bs-target="#test">
<TL>Next</TL>
</a>
</div>
</div>
</div>
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Password</TL>
</div>
</div>
<div class="card-body fs-6">
<div class="d-flex justify-content-center">
<div class="input-group">
<input @bind="Password" class="form-control" type="password"/>
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="ChangePassword">
</WButton>
</div>
</div>
</div>
</div>
<div class="modal fade" id="test" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered mw-650px">
<div class="modal-content">
<div class="modal-header flex-stack py-6">
<h2 class="ms-3">
<TL>Finish activation</TL>
</h2>
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
<span class="svg-icon svg-icon-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="6" y="17.3137" width="16" height="2" rx="1" transform="rotate(-45 6 17.3137)" fill="currentColor"></rect>
<rect x="7.41422" y="6" width="16" height="2" rx="1" transform="rotate(45 7.41422 6)" fill="currentColor"></rect>
</svg>
</span>
</div>
</div>
<div class="modal-body scroll-y ps-10 pe-10 pb-10">
<div class="text-gray-500 fw-semibold fs-6 mb-10">
<div class="alert alert-primary d-flex align-items-center p-5 mb-6">
<i class="bx bx-info-circle fs-2hx text-primary me-4">
<span class="path1"></span><span class="path2"></span>
</i>
<div class="d-flex flex-column">
<h4 class="mb-1 text-primary">
<TL>2fa Code requiered</TL>
</h4>
<span>In order to finish the activation of 2fa, you need to enter the code your 2fa app shows you.</span>
</div>
</div>
<input type="text" class="form-control form-control-lg form-control-solid mb-0" placeholder="@SmartTranslateService.Translate("2fa Code")" @bind="currentTotp"/>
<br/>
<WButton CssClasses="btn btn-primary mb-2 align-self-center text-nowrap float-end" WorkingText="@SmartTranslateService.Translate("Saving")" Text="@SmartTranslateService.Translate("Finish")" OnClick="CheckAndSaveTotp">
</WButton>
</div>
</div>
</div>
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Preferences</TL>
</div>
</div>
<div class="card-body fs-6">
<div class="form-check form-switch">
<input @bind="UserModel.StreamerMode" class="form-check-input" type="checkbox" role="switch" id="streamerModeSwitch">
<label class="form-check-label" for="streamerModeSwitch">
<TL>Streamer mode</TL>
</label>
</div>
</div>
<div class="card-footer">
<div class="text-end">
<WButton Text="@(SmartTranslateService.Translate("Save"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="SavePreferences">
</WButton>
</div>
</div>
</div>
</div>
</div>
@* Modals *@
<div class="modal fade" id="2fa" tabindex="-1" style="display: none" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">
<TL>Activate 2fa</TL>
</h2>
</div>
<div class="modal-body fs-6">
@if (!User.TotpEnabled)
{
if (string.IsNullOrEmpty(User.TotpSecret))
{
<p>
<TL>Make sure you have installed one of the following apps on your smartphone and press continue</TL>
</p>
<a href="https://support.google.com/accounts/answer/1066447?hl=en" target="_blank">Google Authenticator</a>
<br/>
<a href="https://www.microsoft.com/en-us/account/authenticator" target="_blank">Microsoft Authenticator</a>
<br/>
<a href="https://authy.com/download/" target="_blank">Authy</a>
<br/>
<a href="https://support.1password.com/one-time-passwords/" target="_blank">1Password</a>
<br/>
<div class="separator mt-2"></div>
<div class="alert alert-danger d-flex rounded ms-6 me-6 mt-6 mb-8 bg-body">
<div class="w-100">
<table>
<tr>
<td>
<span class="svg-icon svg-icon-2tx svg-icon-body">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-25">
<span class="text-gray-700 fw-semibold fs-6 ms-4 me-2">
<TL>New password</TL>
</span>
</td>
<td class="w-75">
<input @bind="Password" type="password" class="form-control">
</td>
<td class="">
<WButton OnClick="ChangePassword"
CssClasses="btn-danger ms-4"
Text="@SmartTranslateService.Translate("Change")"
WorkingText="@SmartTranslateService.Translate("Changing")">
<div class="d-flex justify-content-center">
<WButton Text="@(SmartTranslateService.Translate("Continue"))"
WorkingText="@(SmartTranslateService.Translate("Preparing"))"
CssClasses="btn-primary"
OnClick="GenerateTwoFactorToken">
</WButton>
</td>
</tr>
</table>
</div>
}
else
{
<p>
<TL>Scan the qr code and enter the code generated by the app you have scanned it in</TL>
</p>
<div class="mt-3 text-center">
@{
QRCodeGenerator qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode
(
$"otpauth://totp/{Uri.EscapeDataString(User.Email)}?secret={User.TotpSecret}&issuer={Uri.EscapeDataString("Moonlight")}",
QRCodeGenerator.ECCLevel.Q
);
PngByteQRCode qrCode = new PngByteQRCode(qrCodeData);
byte[] qrCodeAsPngByteArr = qrCode.GetGraphic(20);
var base64 = Convert.ToBase64String(qrCodeAsPngByteArr);
}
<img src="data:image/png;base64,@(base64)" alt="" class="mw-200px mt-2">
</div>
<div class="mt-3 d-flex justify-content-center">
<div class="input-group">
<input type="text"
@bind="TwoFactorCode"
placeholder="@(SmartTranslateService.Translate("Enter your 2fa code here"))"
class="form-control"/>
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-primary"
OnClick="EnableTwoFactor">
</WButton>
</div>
</div>
}
}
</div>
</div>
</LazyLoader>
</div>
</div>
@code
{
private bool TotpEnabled = false;
private bool EnablingTotp = false;
private string TotpSecret = "";
private User User;
private string Issuer = "Moonlight";
private string currentTotp = "";
[CascadingParameter]
public User User { get; set; }
private string TwoFactorCode = "";
private string Password = "";
private UserPreferencesDataModel UserModel;
private async void Enable()
protected override void OnParametersSet()
{
//TODO: AuditLog
await TotpService.Enable();
TotpEnabled = await TotpService.GetEnabled();
TotpSecret = await TotpService.GetSecret();
EnablingTotp = true;
StateHasChanged();
UserModel = Mapper.Map<UserPreferencesDataModel>(User);
}
public async Task CheckAndSaveTotp()
private async Task StartTwoFactorWizard()
{
if (await TotpService.Verify(TotpSecret, currentTotp))
{
await TotpService.EnforceTotpLogin();
TotpEnabled = true;
TotpSecret = await TotpService.GetSecret();
await ToastService.Success("Successfully enabled 2fa!");
StateHasChanged();
}
else
{
await AlertService.Error("2fa code incorrect", "The given 2fa code is incorrect. Maybe check if the code in your 2fa app has changed.");
}
await ModalService.Show("2fa");
}
private async void Disable()
private async Task GenerateTwoFactorToken()
{
//TODO: AuditLog
await TotpService.Disable();
await TotpService.GenerateSecret();
await InvokeAsync(StateHasChanged);
}
private async Task EnableTwoFactor()
{
await ModalService.Hide("2fa");
await TotpService.Enable(TwoFactorCode);
await InvokeAsync(StateHasChanged);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
private async Task Load(LazyLoader lazyLoader)
private async Task DisableTwoFactor()
{
await lazyLoader.SetText("Requesting secrets");
TotpEnabled = await TotpService.GetEnabled();
TotpSecret = await TotpService.GetSecret();
await lazyLoader.SetText("Requesting identity");
User = await IdentityService.Get();
await TotpService.Disable();
await InvokeAsync(StateHasChanged);
}
private async Task ChangePassword()
{
if (Regex.IsMatch(Password, @"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z@$!%*#.,?&\d]{8,}$"))
{
await UserService.ChangePassword(User, Password);
await UserService.ChangePassword(User, Password);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
//TODO: AuditLog
// Reload to make the user login again
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
else
{
await AlertService.Error("Error", "Your password must be at least 8 characters and must contain a number");
}
private async Task SavePreferences()
{
User = Mapper.Map(User, UserModel);
UserRepository.Update(User);
await InvokeAsync(StateHasChanged);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}

View File

@@ -47,7 +47,7 @@
<div class="row align-items-center">
<div class="col fs-5">
<span class="fw-bold"><TL>Shared IP</TL>:</span>
<span class="ms-1 text-muted">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}")</span>
<span class="ms-1 text-muted @(User.StreamerMode ? "blur" : "")">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}")</span>
</div>
<div class="col fs-5">
<span class="fw-bold"><TL>Server ID</TL>:</span>

View File

@@ -1,13 +1,360 @@
@page "/servers"
@using Moonlight.App.Repositories.Servers
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Newtonsoft.Json
@inject ServerRepository ServerRepository
@inject Repository<Server> ServerRepository
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
@inject IServiceScopeFactory ServiceScopeFactory
@inject IJSRuntime JsRuntime
<LazyLoader Load="Load">
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="mx-auto">
<div class="card card-body">
<div class="d-flex justify-content-between">
<span class="badge badge-primary badge-lg px-5 me-4">Beta</span>
@if (EditMode)
{
<div>
<WButton Text="@(SmartTranslateService.Translate("New group"))"
WorkingText=""
CssClasses="btn-primary me-3"
OnClick="AddGroup">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Finish editing layout"))"
CssClasses="btn-secondary"
OnClick="async () => await SetEditMode(false)">
</WButton>
</div>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Edit layout"))"
CssClasses="btn-secondary"
OnClick="async () => await SetEditMode(true)">
</WButton>
}
</div>
</div>
@foreach (var group in ServerGroups)
{
<div class="accordion my-3" id="serverListGroup@(group.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="serverListGroup-header@(group.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#serverListGroup-body@(group.GetHashCode())" aria-expanded="false" aria-controls="serverListGroup-body@(group.GetHashCode())">
<div class="d-flex justify-content-between">
<div>
@if (EditMode)
{
<input @bind="group.Name" class="form-control"/>
}
else
{
if (string.IsNullOrEmpty(group.Name))
{
<TL>Unsorted servers</TL>
}
else
{
<span>@(group.Name)</span>
}
}
</div>
<div>
@if (EditMode)
{
<WButton Text="@(SmartTranslateService.Translate("Remove group"))"
WorkingText=""
CssClasses="btn-danger"
OnClick="async () => await RemoveGroup(group)">
</WButton>
}
</div>
</div>
</button>
</h2>
<div id="serverListGroup-body@(group.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverListGroup-header@(group.GetHashCode())" data-bs-parent="#serverListGroup">
<div class="accordion-body">
<div class="row min-h-200px draggable-zone" ml-server-group="@(group.Name)">
@foreach (var id in group.Servers)
{
var server = AllServers.First(x => x.Id.ToString() == id);
<div class="col-12 col-md-3 p-3 draggable" ml-server-id="@(server.Id)">
@if (EditMode)
{
<div class="card bg-secondary">
<div class="card-header">
<div class="card-title">
<span class="card-label">@(server.Name)</span>
</div>
<div class="card-toolbar">
<a href="#" class="btn btn-icon btn-sm btn-hover-light-primary draggable-handle">
<i class="bx bx-md bx-move"></i>
</a>
</div>
</div>
<div class="card-body">
<TL>Hidden in edit mode</TL>
</div>
</div>
}
else
{
<a class="invisible-a" href="/server/@(server.Uuid)">
<div class="card bg-secondary">
<div class="card-header">
<div class="card-title">
<span class="card-label">@(server.Name)</span>
</div>
</div>
<div class="card-body">
<span class="card-text fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
<div class="card-text my-1 fs-6 fw-bold @(User.StreamerMode ? "blur" : "")">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="card-text fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
}
</div>
</div>
</div>
</a>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
}
</div>
</LazyLoader>
@code
{
[CascadingParameter]
public User User { get; set; }
private Server[] AllServers;
private LazyLoader LazyLoader;
private readonly Dictionary<Server, string> StatusCache = new();
private List<ServerGroup> ServerGroups = new();
private bool EditMode = false;
private async Task Load(LazyLoader arg)
{
AllServers = ServerRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.MainAllocation)
.Include(x => x.Node)
.Include(x => x.Image)
.Where(x => x.Owner.Id == User.Id)
.OrderBy(x => x.Name)
.ToArray();
if (string.IsNullOrEmpty(User.ServerListLayoutJson))
{
ServerGroups.Add(new()
{
Name = "",
Servers = AllServers.Select(x => x.Id.ToString()).ToList()
});
}
else
{
ServerGroups = (JsonConvert.DeserializeObject<ServerGroup[]>(
User.ServerListLayoutJson) ?? Array.Empty<ServerGroup>()).ToList();
}
foreach (var server in AllServers)
{
Task.Run(async () =>
{
try
{
using var scope = ServiceScopeFactory.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
AddStatus(server, (await serverService.GetDetails(server)).State);
}
catch (Exception e)
{
AddStatus(server, "failed");
}
});
}
}
private async Task AddGroup()
{
ServerGroups.Insert(0, new()
{
Name = "New group"
});
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
private async Task RemoveGroup(ServerGroup group)
{
ServerGroups.Remove(group);
await EnsureAllServersInGroups();
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
private async Task SetEditMode(bool toggle)
{
EditMode = toggle;
await InvokeAsync(StateHasChanged);
if (EditMode)
{
await EnsureAllServersInGroups();
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
else
{
var json = JsonConvert.SerializeObject(await GetGroupsFromClient());
User.ServerListLayoutJson = json;
UserRepository.Update(User);
await LazyLoader.Reload();
}
}
private async Task<ServerGroup[]> GetGroupsFromClient()
{
var serverGroups = await JsRuntime.InvokeAsync<ServerGroup[]>("moonlight.serverList.getData");
// Check user data to prevent users from doing stupid stuff
foreach (var serverGroup in serverGroups)
{
if (serverGroup.Name.Length > 30)
{
Logger.Verbose("Server list group lenght too long");
return Array.Empty<ServerGroup>();
}
if (serverGroup.Servers.Any(x => AllServers.All(y => y.Id.ToString() != x)))
{
Logger.Verbose("User tried to add a server in his server list which he has no access to");
return Array.Empty<ServerGroup>();
}
}
return serverGroups;
}
private Task EnsureAllServersInGroups()
{
var presentInGroup = new List<Server>();
foreach (var group in ServerGroups)
{
foreach (var id in group.Servers)
presentInGroup.Add(AllServers.First(x => x.Id.ToString() == id));
}
var serversMissing = new List<Server>();
foreach (var server in AllServers)
{
if (presentInGroup.All(x => x.Id != server.Id))
serversMissing.Add(server);
}
if (serversMissing.Any())
{
var defaultGroup = ServerGroups.FirstOrDefault(x => x.Name == "");
if (defaultGroup == null)
{
defaultGroup = new ServerGroup()
{
Name = ""
};
ServerGroups.Add(defaultGroup);
}
foreach (var server in serversMissing)
defaultGroup.Servers.Add(server.Id.ToString());
}
return Task.CompletedTask;
}
private void AddStatus(Server server, string status)
{
lock (StatusCache)
{
StatusCache.Add(server, status);
InvokeAsync(StateHasChanged);
}
}
}
@*
@if (AllServers.Any())
{
if (UseSortedServerView)
@@ -207,57 +554,4 @@ else
</div>
</div>
</div>
</LazyLoader>
@code
{
[CascadingParameter]
public User User { get; set; }
private Server[] AllServers;
private readonly Dictionary<Server, string> StatusCache = new();
private bool UseSortedServerView = false;
private Task Load(LazyLoader arg)
{
AllServers = ServerRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.MainAllocation)
.Include(x => x.Node)
.Include(x => x.Image)
.Where(x => x.Owner.Id == User.Id)
.OrderBy(x => x.Name)
.ToArray();
foreach (var server in AllServers)
{
Task.Run(async () =>
{
try
{
using var scope = ServiceScopeFactory.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
AddStatus(server, (await serverService.GetDetails(server)).State);
}
catch (Exception e)
{
AddStatus(server, "failed");
}
});
}
return Task.CompletedTask;
}
private void AddStatus(Server server, string status)
{
lock (StatusCache)
{
StatusCache.Add(server, status);
InvokeAsync(StateHasChanged);
}
}
}
*@

View File

@@ -16,6 +16,10 @@
filter: none;
}
.blur {
filter: blur(5px);
}
div.wave {
}
div.wave .dot {

View File

@@ -355,8 +355,6 @@
{
// filter here what key events should be sent to moonlight
console.log(event);
if(event.code === "KeyS" && event.ctrlKey)
{
event.preventDefault();
@@ -370,5 +368,54 @@
{
window.removeEventListener('keydown', moonlight.keyListener.listener);
}
},
serverList: {
init: function ()
{
if(moonlight.serverList.Swappable)
{
moonlight.serverList.Swappable.destroy();
}
let containers = document.querySelectorAll(".draggable-zone");
if (containers.length !== 0)
{
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
draggable: ".draggable",
handle: ".draggable .draggable-handle",
mirror: {
//appendTo: selector,
appendTo: "body",
constrainDimensions: true
}
});
}
},
getData: function ()
{
let groups = new Array();
let groupElements = document.querySelectorAll('[ml-server-group]');
groupElements.forEach(groupElement => {
let group = new Object();
group.name = groupElement.attributes.getNamedItem("ml-server-group").value;
let servers = new Array();
let serverElements = groupElement.querySelectorAll("[ml-server-id]");
serverElements.forEach(serverElement => {
let id = serverElement.attributes.getNamedItem("ml-server-id").value;
servers.push(id);
});
group.servers = servers;
groups.push(group);
});
return groups;
}
}
};

View File

@@ -67,6 +67,10 @@ Moonlight:
Daemon (not wings):
`curl https://install.moonlightpanel.xyz/daemon| bash`
Having any issues?
We are happy to help on our discord server:
[https://discord.gg/TJaspT7A8p](https://discord.gg/TJaspT7A8p)
## Roadmap
The roudmap can be found here:
@@ -96,3 +100,31 @@ Distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
* **Daniel Balk** - *Endelon Hosting* - [Daniel Balk](https://github.com/Daniel-Balk) - *Notification system & frontend*
* **Spielepapagei** - *Endelon Hosting* - [Spielepapagei](https://github.com/Spielepapagei) - *Discord Bot & support tickets*
* **Dannyx** - *None* - [Dannyx](https://github.com/Dannyx1604) - *Grammer check and translations*
## Some screenshots
Only user area
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635286443634768/dashboard.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635662475571261/serverlist_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635784685002762/console_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635898933657741/filemanager_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636024162992128/filemanager_move_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636204358672494/filemanager_editor.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636339285237820/backups.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636510182150215/addons.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636623784890519/settings.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636741170855967/webspace_overview.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636848670875668/webspace_files.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636950953177138/webspace_databases.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121637134797918259/domains_.png)