Refactored css classes to match flyonui. Switched to postgres arrays for permissions. Migrated file manager. Adjusted everything to work with the latest mooncore version

This commit is contained in:
2025-07-12 23:53:43 +02:00
parent eaece9e334
commit d88376f2fb
72 changed files with 2870 additions and 2227 deletions

View File

@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.7" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
</ItemGroup>
</Project>

View File

@@ -8,12 +8,9 @@ public class ApiKey
public string Description { get; set; }
[Column(TypeName="jsonb")]
public string PermissionsJson { get; set; } = "[]";
public string[] Permissions { get; set; } = [];
[Column(TypeName = "timestamp with time zone")]
public DateTime ExpiresAt { get; set; }
public DateTimeOffset ExpiresAt { get; set; }
[Column(TypeName = "timestamp with time zone")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}

View File

@@ -9,10 +9,6 @@ public class User
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
[Column(TypeName="timestamp with time zone")]
public DateTime TokenValidTimestamp { get; set; } = DateTime.MinValue;
[Column(TypeName="jsonb")]
public string PermissionsJson { get; set; } = "[]";
public DateTimeOffset TokenValidTimestamp { get; set; } = DateTimeOffset.MinValue;
public string[] Permissions { get; set; } = [];
}

View File

@@ -0,0 +1,393 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20250712202608_SwitchedToPgArraysForPermissions")]
partial class SwitchedToPgArraysForPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
.WithMany()
.HasForeignKey("StateId");
b.Navigation("State");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("Parameters")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("QueuedJobs")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("States")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Navigation("Parameters");
b.Navigation("QueuedJobs");
b.Navigation("States");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class SwitchedToPgArraysForPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_Users",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_ApiKeys",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_Users",
type: "jsonb",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_ApiKeys",
type: "jsonb",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Moonlight.ApiServer.Database.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -283,19 +283,19 @@ namespace Moonlight.ApiServer.Database.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("jsonb");
.HasColumnType("text[]");
b.HasKey("Id");
@@ -318,11 +318,11 @@ namespace Moonlight.ApiServer.Database.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("jsonb");
.HasColumnType("text[]");
b.Property<DateTime>("TokenValidTimestamp")
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")

View File

@@ -45,7 +45,7 @@ public class ApiKeysController : Controller
.Select(x => new ApiKeyResponse()
{
Id = x.Id,
PermissionsJson = x.PermissionsJson,
Permissions = x.Permissions,
Description = x.Description,
ExpiresAt = x.ExpiresAt
})
@@ -75,7 +75,7 @@ public class ApiKeysController : Controller
return new ApiKeyResponse()
{
Id = apiKey.Id,
PermissionsJson = apiKey.PermissionsJson,
Permissions = apiKey.Permissions,
Description = apiKey.Description,
ExpiresAt = apiKey.ExpiresAt
};
@@ -88,7 +88,7 @@ public class ApiKeysController : Controller
var apiKey = new ApiKey()
{
Description = request.Description,
PermissionsJson = request.PermissionsJson,
Permissions = request.Permissions,
ExpiresAt = request.ExpiresAt
};
@@ -97,7 +97,7 @@ public class ApiKeysController : Controller
var response = new CreateApiKeyResponse
{
Id = finalApiKey.Id,
PermissionsJson = finalApiKey.PermissionsJson,
Permissions = finalApiKey.Permissions,
Description = finalApiKey.Description,
ExpiresAt = finalApiKey.ExpiresAt,
Secret = ApiKeyService.GenerateJwt(finalApiKey)
@@ -125,7 +125,7 @@ public class ApiKeysController : Controller
{
Id = apiKey.Id,
Description = apiKey.Description,
PermissionsJson = apiKey.PermissionsJson,
Permissions = apiKey.Permissions,
ExpiresAt = apiKey.ExpiresAt
};
}

View File

@@ -17,7 +17,39 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
public class FilesController : Controller
{
private readonly string BaseDirectory = "storage";
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
private readonly long MaxChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
[HttpPost("touch")]
public async Task CreateFile([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
if (System.IO.File.Exists(physicalPath))
throw new HttpApiException("A file already exists at that path", 400);
if (Directory.Exists(path))
throw new HttpApiException("A folder already exists at that path", 400);
await using var fs = System.IO.File.Create(physicalPath);
fs.Close();
}
[HttpPost("mkdir")]
public Task CreateFolder([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
if (Directory.Exists(path))
throw new HttpApiException("A folder already exists at that path", 400);
if (System.IO.File.Exists(physicalPath))
throw new HttpApiException("A file already exists at that path", 400);
Directory.CreateDirectory(physicalPath);
return Task.CompletedTask;
}
[HttpGet("list")]
public Task<FileSystemEntryResponse[]> List([FromQuery] string path)
@@ -38,7 +70,7 @@ public class FilesController : Controller
Name = fi.Name,
Size = fi.Length,
CreatedAt = fi.CreationTimeUtc,
IsFile = true,
IsFolder = false,
UpdatedAt = fi.LastWriteTimeUtc
});
}
@@ -55,7 +87,7 @@ public class FilesController : Controller
Size = 0,
CreatedAt = di.CreationTimeUtc,
UpdatedAt = di.LastWriteTimeUtc,
IsFile = false
IsFolder = true
});
}
@@ -65,23 +97,23 @@ public class FilesController : Controller
}
[HttpPost("upload")]
public async Task Upload([FromQuery] string path, [FromQuery] long totalSize, [FromQuery] int chunkId)
public async Task Upload([FromQuery] string path, [FromQuery] long chunkSize, [FromQuery] long totalSize, [FromQuery] int chunkId)
{
if (Request.Form.Files.Count != 1)
throw new HttpApiException("You need to provide exactly one file", 400);
var file = Request.Form.Files[0];
if (file.Length > ChunkSize)
if (file.Length > chunkSize)
throw new HttpApiException("The provided data exceeds the chunk size limit", 400);
var chunks = totalSize / ChunkSize;
chunks += totalSize % ChunkSize > 0 ? 1 : 0;
var chunks = totalSize / chunkSize;
chunks += totalSize % chunkSize > 0 ? 1 : 0;
if (chunkId > chunks)
throw new HttpApiException("Invalid chunk id: Out of bounds", 400);
var positionToSkipTo = ChunkSize * chunkId;
var positionToSkipTo = chunkSize * chunkId;
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
@@ -156,16 +188,6 @@ public class FilesController : Controller
return Task.CompletedTask;
}
[HttpPost("mkdir")]
public Task CreateDirectory([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
Directory.CreateDirectory(physicalPath);
return Task.CompletedTask;
}
[HttpGet("download")]
public async Task Download([FromQuery] string path)
{
@@ -431,5 +453,23 @@ public class FilesController : Controller
#endregion
private string SanitizePath(string path)
=> path.Replace("..", "");
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
// Normalize separators
path = path.Replace('\\', '/');
// Remove ".." and "."
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Where(part => part != ".." && part != ".");
var sanitized = string.Join("/", parts);
// Ensure it does not start with a slash
if (sanitized.StartsWith('/'))
sanitized = sanitized.TrimStart('/');
return sanitized;
}
}

View File

@@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Attributes;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Responses.Admin.Sys;
@@ -12,13 +10,10 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
public class SystemController : Controller
{
private readonly ApplicationService ApplicationService;
private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders;
public SystemController(ApplicationService applicationService, IEnumerable<IDiagnoseProvider> diagnoseProviders)
public SystemController(ApplicationService applicationService)
{
ApplicationService = applicationService;
DiagnoseProviders = diagnoseProviders;
}
[HttpGet]

View File

@@ -45,7 +45,7 @@ public class UsersController : Controller
Id = x.Id,
Email = x.Email,
Username = x.Username,
PermissionsJson = x.PermissionsJson
Permissions = x.Permissions
})
.ToArray();
@@ -75,7 +75,7 @@ public class UsersController : Controller
Id = user.Id,
Email = user.Email,
Username = user.Username,
PermissionsJson = user.PermissionsJson
Permissions = user.Permissions
};
}
@@ -101,7 +101,7 @@ public class UsersController : Controller
Email = request.Email,
Username = request.Username,
Password = hashedPassword,
PermissionsJson = request.PermissionsJson
Permissions = request.Permissions
};
var finalUser = await UserRepository.Add(user);
@@ -111,7 +111,7 @@ public class UsersController : Controller
Id = finalUser.Id,
Email = finalUser.Email,
Username = finalUser.Username,
PermissionsJson = finalUser.PermissionsJson
Permissions = finalUser.Permissions
};
}
@@ -144,9 +144,9 @@ public class UsersController : Controller
user.TokenValidTimestamp = DateTime.UtcNow; // Log out user after password change
}
if (user.PermissionsJson != request.PermissionsJson)
if (request.Permissions.Any(x => !user.Permissions.Contains(x)))
{
user.PermissionsJson = request.PermissionsJson;
user.Permissions = request.Permissions;
user.TokenValidTimestamp = DateTime.UtcNow; // Log out user after permission change
}
@@ -160,7 +160,7 @@ public class UsersController : Controller
Id = user.Id,
Email = user.Email,
Username = user.Username,
PermissionsJson = user.PermissionsJson
Permissions = user.Permissions
};
}

View File

@@ -73,9 +73,6 @@ public class AuthController : Controller
if (user == null)
throw new HttpApiException("Unable to load user data", 500);
//
var permissions = JsonSerializer.Deserialize<string[]>(user.PermissionsJson) ?? [];
// Generate token
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
@@ -90,7 +87,7 @@ public class AuthController : Controller
},
{
"permissions",
string.Join(";", permissions)
string.Join(";", user.Permissions)
}
},
SigningCredentials = new SigningCredentials(
@@ -122,13 +119,11 @@ public class AuthController : Controller
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
var permissions = JsonSerializer.Deserialize<string[]>(user.PermissionsJson) ?? [];
return new()
{
Email = user.Email,
Username = user.Username,
Permissions = string.Join(";", permissions)
Permissions = user.Permissions
};
}
}

View File

@@ -1,6 +1,6 @@
@using Moonlight.Shared.Misc
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="bg-background text-base-content font-inter">
<head>
<meta charset="utf-8" />
@@ -18,15 +18,15 @@
<link rel="apple-touch-icon" sizes="192x192" href="/img/icon-192.png" />
</head>
<body class="bg-gray-950 text-white font-inter h-full">
<body>
<div id="app">
<div class="flex h-screen justify-center items-center">
<div class="sm:max-w-lg">
<div id="blazor-loader-label" class="text-center mb-2 text-lg font-semibold"></div>
<div class="flex flex-col gap-1">
<div class="progress min-w-sm md:min-w-md" role="progressbar">
<div id="blazor-loader-progress" class="progress-bar"></div>
<div class="progress h-3 min-w-sm md:min-w-md" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<div id="blazor-loader-progress" class="progress-bar progress-primary"></div>
</div>
</div>
</div>

View File

@@ -291,7 +291,7 @@ public partial class OAuth2Controller : Controller
var userCount = await UserRepository.Get().CountAsync();
if (userCount == 0)
user.PermissionsJson = "[\"*\"]";
user.Permissions = ["*"];
}

View File

@@ -89,7 +89,7 @@ public class CoreStartup : IPluginStartup
{
Scripts =
[
"/_content/Moonlight.Client/js/moonlight.js", "/_content/Moonlight.Client/js/moonCore.js",
"/_content/Moonlight.Client/js/moonlight.js", "/_content/MoonCore.Blazor.FlyonUi/moonCore.js",
"/_content/Moonlight.Client/ace/ace.js"
],
Styles = ["/css/style.min.css"]

View File

@@ -20,7 +20,7 @@
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.1</Version>
<Version>2.1.2</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -34,9 +34,9 @@
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageReference Include="MoonCore" Version="1.9.1" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Extended" Version="1.3.5" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.1" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
@@ -58,5 +58,6 @@
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
<None Remove="Properties\launchSettings.json" />
</ItemGroup>
</Project>

View File

@@ -1,29 +0,0 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTP_PROXY": "",
"HTTPS_PROXY": ""
},
"hotReloadEnabled": true
},
"WASM Debug": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTP_PROXY": "",
"HTTPS_PROXY": ""
},
"hotReloadEnabled": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
}
}
}

View File

@@ -20,13 +20,11 @@ public class ApiKeyService
public string GenerateJwt(ApiKey apiKey)
{
var permissions = JsonSerializer.Deserialize<string[]>(apiKey.PermissionsJson) ?? [];
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var descriptor = new SecurityTokenDescriptor()
{
Expires = apiKey.ExpiresAt,
Expires = apiKey.ExpiresAt.UtcDateTime,
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(-1),
Claims = new Dictionary<string, object>()
@@ -37,7 +35,7 @@ public class ApiKeyService
},
{
"permissions",
string.Join(";", permissions)
string.Join(";", apiKey.Permissions)
}
},
SigningCredentials = new SigningCredentials(

View File

@@ -12,8 +12,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.7"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.1"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all"/>
@@ -50,6 +50,11 @@
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Styles\mappings\mooncore.map">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>

View File

@@ -1,9 +0,0 @@
@keyframes shimmer {
0% {
background-position: 0 0
}
to {
background-position: -200% 0
}
}

View File

@@ -1,93 +0,0 @@
/* Buttons */
.btn,
.btn-lg,
.btn-sm,
.btn-xs {
@apply cursor-pointer font-medium text-sm inline-flex items-center justify-center border border-transparent rounded-lg leading-5 shadow-sm transition active:scale-95;
}
.btn {
@apply px-3 py-2;
}
.btn-lg {
@apply px-4 py-3;
}
.btn-sm {
@apply px-2.5 py-1.5;
}
.btn-xs {
@apply px-2 py-0.5;
}
/* Colors */
.btn-primary {
@apply bg-primary hover:bg-primary/90 focus-visible:outline-primary text-diffcolor;
}
.btn-secondary {
@apply bg-secondary hover:bg-secondary/90 focus-visible:outline-secondary text-diffcolor;
}
.btn-tertiary {
@apply bg-tertiary hover:bg-tertiary/90 focus-visible:outline-tertiary text-diffcolor;
}
.btn-danger {
@apply bg-danger hover:bg-danger/90 focus-visible:outline-danger text-diffcolor;
}
.btn-warning {
@apply bg-warning hover:bg-warning/90 focus-visible:outline-warning text-diffcolor;
}
.btn-info {
@apply bg-info hover:bg-info/90 focus-visible:outline-info text-diffcolor;
}
.btn-success {
@apply bg-success hover:bg-success/90 focus-visible:outline-success text-diffcolor;
}
/* Disabled Buttons */
.btn:disabled,
.btn-lg:disabled,
.btn-sm:disabled,
.btn-xs:disabled {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
/* Colors for Disabled States */
.btn-primary:disabled {
@apply bg-primary/80 text-gray-300;
}
.btn-secondary:disabled {
@apply bg-secondary/80 text-gray-400;
}
.btn-tertiary:disabled {
@apply bg-tertiary/80 text-gray-300;
}
.btn-danger:disabled {
@apply bg-danger/80 text-gray-300;
}
.btn-warning:disabled {
@apply bg-warning/80 text-gray-400;
}
.btn-info:disabled {
@apply bg-info/80 text-gray-300;
}
.btn-success:disabled {
@apply bg-success/80 text-gray-300;
}

View File

@@ -1,23 +0,0 @@
.card {
@apply flex flex-col bg-gray-800 shadow-sm rounded-xl;
}
.card-header {
@apply p-5;
}
.card-header:has(+ .card-body) {
@apply pb-0;
}
.card-title {
@apply text-2xl font-semibold text-white;
}
.card-body {
@apply p-5 text-gray-200;
}
.card-footer {
@apply p-5;
}

View File

@@ -1,8 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback') layer(base);
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap') layer(base);
@import url("https://cdn.jsdelivr.net/npm/lucide-static/font/lucide.css") layer(base);
@theme {
--font-inter: "Inter", var(--font-sans);
--font-scp: "Source Code Pro", var(--font-mono);
}

View File

@@ -1,77 +0,0 @@
/* Forms */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
-webkit-appearance: none;
}
.form-input,
.form-textarea,
.form-multiselect,
.form-select,
.form-checkbox,
.form-radio {
@apply bg-gray-700/60 border-2 focus:ring-0 focus:ring-offset-0 disabled:bg-gray-700/30 disabled:border-gray-700 disabled:hover:border-gray-700;
}
.form-checkbox {
@apply rounded;
}
.form-input,
.form-textarea,
.form-multiselect,
.form-select {
@apply text-sm text-gray-100 leading-5 py-2 px-3 border-gray-700 focus:border-primary shadow-sm rounded-lg;
}
.form-input,
.form-textarea {
@apply placeholder-gray-700;
}
.form-select {
@apply pr-10;
}
.form-checkbox,
.form-radio {
@apply text-primary checked:bg-primary checked:border-transparent border border-gray-700/60 focus:border-primary/50;
}
/* Switch element */
.form-switch {
@apply relative select-none;
width: 44px;
}
.form-switch label {
@apply block overflow-hidden cursor-pointer h-6 rounded-full;
}
.form-switch label > span:first-child {
@apply absolute block rounded-full;
width: 20px;
height: 20px;
top: 2px;
left: 2px;
right: 50%;
transition: all .15s ease-out;
}
.form-switch input[type="checkbox"]:checked + label {
@apply bg-primary;
}
.form-switch input[type="checkbox"]:checked + label > span:first-child {
left: 22px;
}
.form-switch input[type="checkbox"]:disabled + label {
@apply cursor-not-allowed bg-gray-700/20 border border-gray-700/60;
}
.form-switch input[type="checkbox"]:disabled + label > span:first-child {
@apply bg-gray-600;
}

View File

@@ -1,28 +0,0 @@
.loader-spinner {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
position: relative;
animation: loader-spinner-rotate 1s linear infinite
}
.loader-spinner::before {
content: "";
box-sizing: border-box;
position: absolute;
inset: 0px;
border-radius: 50%;
border: 3px solid #FFF;
animation: loader-spinner-prixClipFix 2s linear infinite ;
}
@keyframes loader-spinner-rotate {
100% {transform: rotate(360deg)}
}
@keyframes loader-spinner-prixClipFix {
0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)}
25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)}
50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)}
75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)}
100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)}
}

View File

@@ -1,25 +0,0 @@
.progress {
@apply bg-gray-800 rounded-full overflow-hidden;
}
.progress-bar {
@apply bg-primary rounded-full h-3;
transition: width 0.6s ease;
}
.progress-bar.progress-intermediate {
animation: progress-animation 1s infinite linear;
transform-origin: 0 50%
}
@keyframes progress-animation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}

View File

@@ -1,9 +0,0 @@
* {
scrollbar-width: thin;
scrollbar-color: #64748b transparent;
}
.no-scrollbar {
scrollbar-width: none;
scrollbar-color: transparent transparent;
}

View File

@@ -1,11 +0,0 @@
.tabs {
@apply flex gap-x-1 bg-gray-800 rounded-lg transition p-1;
}
.tabs .tabs-segment {
@apply cursor-pointer font-medium text-sm inline-flex items-center justify-center border border-transparent rounded-lg leading-5 text-gray-300 hover:text-primary py-1.5 px-3.5;
}
.tabs .tabs-segment-active {
@apply bg-primary hover:bg-primary/90 focus-visible:outline-primary text-diffcolor hover:text-diffcolor;
}

View File

@@ -1,46 +0,0 @@
@theme {
/* Color Variables */
--color-primary: oklch(.511 .262 276.966);
--color-secondary: rgb(31, 41, 55);
--color-tertiary: oklch(.627 .265 303.9);
--color-warning: oklch(.828 .189 84.429);
--color-danger: oklch(.586 .253 17.585);
--color-success: oklch(.627 .194 149.214);
--color-info: oklch(.546 .245 262.881);
/* Gray */
--color-gray-50: #e8eefc;
--color-gray-100: rgb(249 249 249);
--color-gray-200: rgb(241 241 242);
--color-gray-300: rgb(219 223 233);
--color-gray-400: rgb(181 181 195);
--color-gray-500: rgb(153 161 183);
--color-gray-600: rgb(112 121 147);
--color-gray-700: rgb(68 78 107);
--color-gray-750: rgb(41 50 73);
--color-gray-800: rgb(28 36 56);
--color-gray-900: rgb(17 23 33);
--color-gray-950: rgb(14 18 28);
/*
--color-gray-50: #e8eefc;
--color-gray-100: #ccd6ee;
--color-gray-200: #bec9e1;
--color-gray-300: #a3b2d5;
--color-gray-400: #7d91bb;
--color-gray-500: #5f719d;
--color-gray-600: #1a2640;
--color-gray-700: #101a2e;
--color-gray-750: #0f1729;
--color-gray-800: #0c1221;
--color-gray-900: #050a16;
--color-gray-950: #03060e;
*/
/* Full Colors */
--color-white: rgb(255 255 255);
--color-black: rgb(0 0 0);
--color-diffcolor: rgb(var(--color-white));
}

View File

@@ -1,23 +0,0 @@
@theme {
/* Gray (Inverted for White Mode) */
--color-gray-100: rgb(14 18 28); /* Formerly gray-950 */
--color-gray-200: rgb(17 23 33); /* Formerly gray-900 */
--color-gray-300: rgb(28 36 56); /* Formerly gray-800 */
--color-gray-400: rgb(41 50 73); /* Formerly gray-750 */
--color-gray-500: rgb(68 78 107); /* Formerly gray-700 */
--color-gray-600: rgb(112 121 147); /* Formerly gray-600 */
--color-gray-700: rgb(153 161 183); /* Formerly gray-500 */
--color-gray-750: rgb(181 181 195); /* Formerly gray-400 */
--color-gray-800: rgb(219 223 233); /* Formerly gray-300 */
--color-gray-900: rgb(241 241 242); /* Formerly gray-200 */
--color-gray-950: rgb(249 249 249); /* Formerly gray-100 */
/* Full Colors (Inverted) */
--color-white: rgb(0 0 0); /* Inverted to black */
--color-black: rgb(255 255 255); /* Inverted to white */
/* Special light mode stuff */
--color-diffcolor: rgb(255 255 255);
}

View File

@@ -1,26 +0,0 @@
@import "./additions/fonts.css";
@import "./additions/theme.css" layer(theme);
/* @import "./additions/theme.white.css"; */
@import "./additions/buttons.css" layer(components);
@import "./additions/cards.css" layer(components);
@import "./additions/forms.css" layer(components);
@import "./additions/progress.css" layer(components);
@import "./additions/scrollbar.css" layer(components);
@import "./additions/loaders.css" layer(components);
@import "./additions/tabs.css" layer(components);
@source "./mappings/*.map";
#blazor-error-ui {
display: none;
}
#blazor-loader-label:after {
content: var(--blazor-load-percentage-text, "Loading");
}
#blazor-loader-progress {
width: var(--blazor-load-percentage, 0%);
}

View File

@@ -0,0 +1,30 @@
// extract-classes.js
const fs = require('fs');
module.exports = (opts = {}) => {
const classSet = new Set();
return {
postcssPlugin: 'extract-tailwind-classes',
Rule(rule) {
const selectorParser = require('postcss-selector-parser');
selectorParser(selectors => {
selectors.walkClasses(node => {
classSet.add(node.value);
});
}).processSync(rule.selector);
},
OnceExit() {
const classArray = Array.from(classSet).sort();
if (!fs.existsSync("./mappings")){
fs.mkdirSync("./mappings");
}
fs.writeFileSync('./mappings/mooncore.map', classArray.join('\n'));
console.log(`✅ Extracted ${classArray.length} Tailwind classes to tailwind-classes.txt`);
}
};
};
module.exports.postcss = true;

1093
Moonlight.Client.Runtime/Styles/mappings/mooncore.map Normal file → Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,23 @@
{
"dependencies": {
"@tailwindcss/cli": "^4.1.4",
"@tailwindcss/forms": "^0.5.10",
"tailwindcss": "^4.1.4",
"xml2js": "^0.6.2"
},
"name": "styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"pretailwind-build": "node resolveNuget.js ../Moonlight.Client.Runtime.csproj",
"tailwind-build": "npx tailwindcss -i style.css -o ../wwwroot/css/style.min.css",
"pretailwind": "node resolveNuget.js ../Moonlight.Client.Runtime.csproj",
"tailwind": "npx tailwindcss -i style.css -o ../wwwroot/css/style.min.css --watch"
"tailwind": "npx postcss styles.css -o ../wwwroot/css/style.min.css --watch",
"tailwind-build": "npx postcss styles.css -o ../wwwroot/css/style.min.css",
"mappings": "EXTRACT_CLASSES=true npx postcss styles.css -o ../wwwroot/css/style.min.css "
},
"author": "",
"license": "ISC",
"dependencies": {
"@tailwindcss/postcss": "^4.1.11",
"flyonui": "^2.2.0",
"tailwindcss": "^4.1.11",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"postcss-selector-parser": "^7.1.0"
},
"devDependencies": {
}
}

View File

@@ -0,0 +1,11 @@
const tailwindcss = require('@tailwindcss/postcss');
const extractClasses = require('./extract-classes');
module.exports = {
plugins: [
tailwindcss
],
};
if(process.env.EXTRACT_CLASSES === "true")
module.exports.plugins.push(extractClasses);

View File

@@ -1 +0,0 @@
@import "./additions/fonts.css";

View File

@@ -1,80 +0,0 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const xml2js = require('xml2js');
// Helpers
function getPackageRefs(csprojPath) {
const xml = fs.readFileSync(csprojPath, 'utf8');
const parser = new xml2js.Parser();
return new Promise((resolve, reject) => {
parser.parseString(xml, (err, result) => {
if (err) return reject(err);
const itemGroups = result.Project.ItemGroup || [];
const refs = [];
for (const group of itemGroups) {
const packages = group.PackageReference || [];
for (const pkg of packages) {
const name = pkg.$.Include;
const version = pkg.$.Version || (pkg.Version && pkg.Version[0]);
if (name && version) {
refs.push({ name: name.toLowerCase(), version });
}
}
}
resolve(refs);
});
});
}
async function main() {
const csprojPath = process.argv[2];
if (!csprojPath || !fs.existsSync(csprojPath)) {
console.error('Usage: Missing csproj path');
process.exit(1);
}
const nugetPath = path.join(os.homedir(), '.nuget', 'packages');
const moonlightDir = path.join(__dirname, 'node_modules', 'moonlight');
fs.mkdirSync(moonlightDir, { recursive: true });
const refs = await getPackageRefs(csprojPath);
var outputCss = "";
var preOutputCss = "";
for (const { name, version } of refs) {
const packagePath = path.join(nugetPath, name, version);
const exportsFile = path.join(packagePath, 'styles', 'exports.css');
const preTailwindFile = path.join(packagePath, 'styles', 'preTailwind.css');
const sourceFolder = path.join(packagePath, 'src');
const rel = (p) => p.replace(/\\/g, '/');
if (fs.existsSync(exportsFile)) {
outputCss += `@import "${rel(exportsFile)}";\n`;
}
if (fs.existsSync(preTailwindFile)) {
preOutputCss += `@import "${rel(preTailwindFile)}";\n`;
}
if (fs.existsSync(sourceFolder)) {
outputCss += `@source "${rel(path.join(sourceFolder, "**", "*.razor"))}";\n`;
outputCss += `@source "${rel(path.join(sourceFolder, "**", "*.cs"))}";\n`;
outputCss += `@source "${rel(path.join(sourceFolder, "**", "*.html"))}";\n`;
}
}
fs.writeFileSync(path.join(moonlightDir, 'nuget.css'), outputCss);
fs.writeFileSync(path.join(moonlightDir, 'preTailwind.nuget.css'), preOutputCss);
console.log(`Generated nuget.css in ${moonlightDir}`);
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -1,21 +0,0 @@
@import "./preTailwind.css";
@import "moonlight/preTailwind.nuget.css";
@import "tailwindcss";
@import "./exports.css";
@import "moonlight/nuget.css";
@plugin "@tailwindcss/forms" {
strategy: "base";
}
@source "../**/*.razor";
@source "../**/*.cs";
@source "../**/*.html";
@source "../../Moonlight.Client/**/*.razor";
@source "../../Moonlight.Client/**/*.cs";
@source "../../Moonlight.Client/**/*.html";
@source "./mappings/*.map";

View File

@@ -0,0 +1,112 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback') layer(base);
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap') layer()base;
@import url("https://cdn.jsdelivr.net/npm/lucide-static/font/lucide.css") layer(base);
@import "tailwindcss";
@import "./node_modules/flyonui/variants.css";
@import "./theme.css";
@theme {
--font-inter: "Inter", var(--font-sans);
--font-scp: "Source Code Pro", var(--font-mono);
--color-background: var(--mooncore-color-background);
--color-base-150: var(--mooncore-color-base-150);
--color-base-250: var(--mooncore-color-base-250);
}
@plugin "flyonui" {
themes: mooncore --default;
}
@source "./node_modules/flyonui/dist/index.js";
@source "../**/*.razor";
@source "../**/*.cs";
@source "../**/*.html";
@source "../../Moonlight.Client/**/*.cs";
@source "../../Moonlight.Client/**/*.html";
@source "../../Moonlight.Client/**/*.razor";
@source "../../Moonlight.Client/Styles/mappings/*.map";
@source "../../Moonlight.ApiServer/**/*.razor";
#blazor-error-ui {
display: none;
}
#blazor-loader-label:after {
content: var(--blazor-load-percentage-text, "Loading");
}
#blazor-loader-progress {
width: var(--blazor-load-percentage, 0%);
}
@plugin "flyonui/theme" {
name: "mooncore";
default: true;
prefersdark: true;
color-scheme: "dark";
--color-base-100: var(--mooncore-color-base-100);
--color-base-200: var(--mooncore-color-base-200);
--color-base-300: var(--mooncore-color-base-300);
--color-base-content: var(--mooncore-color-base-content);
--color-primary: var(--mooncore-color-primary);
--color-primary-content: var(--mooncore-color-primary-content);
--color-secondary: var(--mooncore-color-secondary);
--color-secondary-content: var(--mooncore-color-secondary-content);
--color-accent: var(--mooncore-color-accent);
--color-accent-content: var(--mooncore-color-accent-content);
--color-neutral: var(--mooncore-color-neutral);
--color-neutral-content: var(--mooncore-color-neutral-content);
--color-info: var(--mooncore-color-info);
--color-info-content: var(--mooncore-color-info-content);
--color-success: var(--mooncore-color-success);
--color-success-content: var(--mooncore-color-success-content);
--color-warning: var(--mooncore-color-warning);
--color-warning-content: var(--mooncore-color-warning-content);
--color-error: var(--mooncore-color-error);
--color-error-content: var(--mooncore-color-error-content);
--radius-selector: var(--mooncore-radius-selector);
--radius-field: var(--mooncore-radius-field);
--radius-box: var(--mooncore-radius-box);
--size-selector: var(--mooncore-size-selector);
--size-field: var(--mooncore-size-field);
--border: var(--mooncore-border);
--depth: var(--mooncore-depth);
--noise: var(--mooncore-noise);
}
@layer utilities {
.btn {
@apply text-sm font-medium inline-flex items-center justify-center;
}
.checkbox {
@apply border-base-content/30 bg-base-100;
}
.input {
@apply !border-base-content/20 border-2 ring-0! outline-0! focus:border-primary! focus-within:border-primary! bg-base-200/50;
}
.advance-select-toggle {
@apply !border-base-content/20 border-2 ring-0! outline-0! focus:border-primary! focus-within:border-primary! bg-base-200/50;
}
.table {
:where(th, td) {
@apply py-1.5;
}
}
.dropdown-item {
@apply px-2.5 py-1.5 text-sm;
}
.dropdown-menu {
@apply bg-base-150;
}
}

View File

@@ -0,0 +1,33 @@
@theme {
--mooncore-color-background: #0c0f18;
--mooncore-color-base-100: #1e2b47;
--mooncore-color-base-150: #1a2640;
--mooncore-color-base-200: #101a2e;
--mooncore-color-base-250: #0f1729;
--mooncore-color-base-300: #0c1221;
--mooncore-color-base-content: #dde5f5;
--mooncore-color-primary: oklch(.511 .262 276.966);
--mooncore-color-primary-content: #dde5f5;
--mooncore-color-secondary: oklch(37% 0.034 259.733);
--mooncore-color-secondary-content: #dde5f5;
--mooncore-color-accent: oklch(.627 .265 303.9);
--mooncore-color-accent-content: #dde5f5;
--mooncore-color-neutral: #dde5f5;
--mooncore-color-neutral-content: oklch(14% 0.005 285.823);
--mooncore-color-info: oklch(.546 .245 262.881);
--mooncore-color-info-content: #dde5f5;
--mooncore-color-success: oklch(.627 .194 149.214);
--mooncore-color-success-content: #dde5f5;
--mooncore-color-warning: oklch(.828 .189 84.429);
--mooncore-color-warning-content: #dde5f5;
--mooncore-color-error: oklch(.586 .253 17.585);
--mooncore-color-error-content: #dde5f5;
--mooncore-radius-selector: 0.25rem;
--mooncore-radius-field: 0.5rem;
--mooncore-radius-box: 0.5rem;
--mooncore-size-selector: 0.25rem;
--mooncore-size-field: 0.25rem;
--mooncore-border: 1px;
--mooncore-depth: 0;
--mooncore-noise: 0;
}

View File

@@ -1,134 +0,0 @@
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Fm;
using MoonCore.Blazor.Tailwind.Fm.Models;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.Client.Implementations;
public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemProvider
{
private readonly DownloadService DownloadService;
private readonly HttpApiClient HttpApiClient;
private readonly LocalStorageService LocalStorageService;
private readonly string BaseApiUrl = "api/admin/system/files";
public CompressType[] CompressTypes { get; } =
[
new()
{
Extension = "zip",
DisplayName = "ZIP Archive"
},
new()
{
Extension = "tar.gz",
DisplayName = "GZ Compressed Tar Archive"
}
];
public SysFileSystemProvider(
HttpApiClient httpApiClient,
DownloadService downloadService,
LocalStorageService localStorageService
)
{
HttpApiClient = httpApiClient;
DownloadService = downloadService;
LocalStorageService = localStorageService;
}
public async Task<FileSystemEntry[]> List(string path)
{
var entries = await HttpApiClient.GetJson<FileSystemEntryResponse[]>(
$"{BaseApiUrl}/list?path={path}"
);
return entries.Select(x => new FileSystemEntry()
{
Name = x.Name,
Size = x.Size,
CreatedAt = x.CreatedAt,
IsFile = x.IsFile,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
public async Task Create(string path, Stream stream)
{
await Upload(_ => Task.CompletedTask, path, stream);
}
public async Task Move(string oldPath, string newPath)
=> await HttpApiClient.Post($"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}");
public async Task Delete(string path)
=> await HttpApiClient.Delete($"{BaseApiUrl}/delete?path={path}");
public async Task CreateDirectory(string path)
=> await HttpApiClient.Post($"{BaseApiUrl}/mkdir?path={path}");
public async Task<Stream> Read(string path)
=> await HttpApiClient.GetStream($"{BaseApiUrl}/download?path={path}");
public async Task Download(Func<int, Task> updateProgress, string path, string fileName)
{
var accessToken = await LocalStorageService.GetString("AccessToken");
await DownloadService.DownloadUrl(fileName, $"{BaseApiUrl}/download?path={path}",
async (loaded, total) =>
{
var percent = total == 0 ? 0 : (int)Math.Round((float)loaded / total * 100);
await updateProgress.Invoke(percent);
},
onConfigureHeaders: headers => { headers.Add("Authorization", $"Bearer {accessToken}"); }
);
}
public async Task Upload(Func<int, Task> updateProgress, string path, Stream stream)
{
var size = stream.Length;
var chunkSize = ByteConverter.FromMegaBytes(20).Bytes;
var chunks = size / chunkSize;
chunks += size % chunkSize > 0 ? 1 : 0;
for (var chunkId = 0; chunkId < chunks; chunkId++)
{
var percent = (int)Math.Round((chunkId + 1f) / chunks * 100);
await updateProgress.Invoke(percent);
var buffer = new byte[chunkSize];
var bytesRead = await stream.ReadAsync(buffer);
var uploadForm = new MultipartFormDataContent();
uploadForm.Add(new ByteArrayContent(buffer, 0, bytesRead), "file", path);
await HttpApiClient.Post(
$"{BaseApiUrl}/upload?path={path}&totalSize={size}&chunkId={chunkId}",
uploadForm
);
}
}
public async Task Compress(CompressType type, string path, string[] itemsToCompress)
{
await HttpApiClient.Post($"{BaseApiUrl}/compress", new CompressRequest()
{
Type = type.Extension,
Path = path,
ItemsToCompress = itemsToCompress
});
}
public async Task Decompress(CompressType type, string path, string destination)
{
await HttpApiClient.Post($"{BaseApiUrl}/decompress", new DecompressRequest()
{
Type = type.Extension,
Path = path,
Destination = destination
});
}
}

View File

@@ -0,0 +1,88 @@
using MoonCore.Blazor.FlyonUi.Files;
using MoonCore.Blazor.FlyonUi.Files.Manager;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.Client.Implementations;
public class SystemFsAccess : IFsAccess
{
private readonly HttpApiClient ApiClient;
private const string BaseApiUrl = "api/admin/system/files";
public SystemFsAccess(HttpApiClient apiClient)
{
ApiClient = apiClient;
}
public async Task CreateFile(string path)
{
await ApiClient.Post(
$"{BaseApiUrl}/touch?path={path}"
);
}
public async Task CreateDirectory(string path)
{
await ApiClient.Post(
$"{BaseApiUrl}/mkdir?path={path}"
);
}
public async Task<FsEntry[]> List(string path)
{
var entries = await ApiClient.GetJson<FileSystemEntryResponse[]>(
$"{BaseApiUrl}/list?path={path}"
);
return entries.Select(x => new FsEntry()
{
Name = x.Name,
CreatedAt = x.CreatedAt,
IsFolder = x.IsFolder,
Size = x.Size,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
public async Task Move(string oldPath, string newPath)
{
await ApiClient.Post(
$"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}"
);
}
public Task Read(string path, Func<Stream, Task> onHandleData)
{
throw new NotImplementedException();
}
public Task Write(string path, Stream dataStream)
{
throw new NotImplementedException();
}
public async Task Delete(string path)
{
await ApiClient.Delete(
$"{BaseApiUrl}/delete?path={path}"
);
}
public async Task UploadChunk(string path, int chunkId, long chunkSize, long totalSize, byte[] data)
{
using var formContent = new MultipartFormDataContent();
formContent.Add(new ByteArrayContent(data), "file", "file");
await ApiClient.Post(
$"{BaseApiUrl}/upload?path={path}&chunkId={chunkId}&chunkSize={chunkSize}&totalSize={totalSize}",
formContent
);
}
public Task<byte[]> DownloadChunk(string path, int chunkId, long chunkSize)
{
throw new NotImplementedException();
}
}

View File

@@ -12,7 +12,7 @@
<PropertyGroup>
<PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId>
<Version>2.1.1</Version>
<Version>2.1.2</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -22,9 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
<PackageReference Include="MoonCore" Version="1.9.1" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.4" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.7" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
@@ -58,6 +58,11 @@
<Folder Include="Styles\" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\style.min.css" />
<_ContentIncludedByDefault Remove="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>
<None Include="Styles/mappings/*" Pack="true" PackagePath="Styles/mappings/" />
<None Include="Moonlight.Client.targets" Pack="true" PackagePath="build\Moonlight.Client.targets" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<Project>
<ItemGroup>
<StylesFilesToCopy Include="$(MSBuildThisFileDirectory)../Styles/**/*.*"/>
</ItemGroup>
<Target Name="CopyContent" BeforeTargets="Build">
<Copy SourceFiles="@(StylesFilesToCopy)" DestinationFolder="$(ProjectDir)Styles/%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -1,14 +0,0 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -104,7 +104,7 @@ public class RemoteAuthStateManager : AuthenticationStateManager
[
new Claim("username", checkData.Username),
new Claim("email", checkData.Email),
new Claim("permissions", checkData.Permissions)
new Claim("permissions", string.Join(";", checkData.Permissions))
],
"RemoteAuthStateManager"
)

View File

@@ -0,0 +1,513 @@
!bg-base-100
!border-base-content/40
!border-none
!flex
!font-medium
!font-semibold
!h-2.5
!justify-between
!me-1.5
!ms-auto
!px-2.5
!rounded-full
!text-sm
!w-2.5
*:[grid-area:1/1]
*:first:rounded-tl-lg
*:last:rounded-tr-lg
-left-4
-ml-4
-translate-x-full
-translate-y-1/2
absolute
accordion
accordion-bordered
accordion-toggle
active
active-tab:bg-primary
active-tab:text-base-content
advance-select-menu
advance-select-option
advance-select-tag
advance-select-toggle
alert
alert-error
alert-outline
alert-soft
align-bottom
align-middle
animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
avatar-away-bottom
avatar-away-top
avatar-busy-bottom
avatar-busy-top
avatar-offline-bottom
avatar-offline-top
avatar-online-bottom
avatar-online-top
avatar-placeholder
badge
badge-error
badge-info
badge-outline
badge-primary
badge-soft
badge-success
bg-background
bg-background/60
bg-base-100
bg-base-150
bg-base-200
bg-base-200/50
bg-base-300
bg-base-300/45
bg-base-300/50
bg-base-300/60
bg-error
bg-info
bg-primary
bg-primary/5
bg-success
bg-transparent
bg-warning
block
blur
border
border-0
border-2
border-b
border-base-content
border-base-content/20
border-base-content/40
border-base-content/5
border-dashed
border-t
border-transparent
bottom-0
bottom-full
break-words
btn
btn-accent
btn-active
btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
btn-soft
btn-square
btn-success
btn-text
btn-warning
card
card-alert
card-body
card-border
card-footer
card-header
card-title
carousel
carousel-body
carousel-next
carousel-prev
carousel-slide
chat
chat-avatar
chat-bubble
chat-footer
chat-header
chat-receiver
chat-sender
checkbox
checkbox-primary
checkbox-xs
col-span-1
collapse
combo-box-selected:block
combo-box-selected:dropdown-active
complete
container
contents
cursor-default
cursor-not-allowed
cursor-pointer
diff
disabled
divide-base-150/60
divide-y
drop-shadow
dropdown
dropdown-disabled
dropdown-item
dropdown-menu
dropdown-open:opacity-100
dropdown-open:rotate-180
dropdown-toggle
duration-300
duration-500
ease-in-out
ease-linear
end-3
file-upload-complete:progress-success
fill-black
filter
filter-reset
fixed
flex
flex-1
flex-col
flex-grow
flex-nowrap
flex-row
flex-shrink-0
flex-wrap
focus-visible:outline-none
focus-within:border-primary
focus:border-primary
focus:outline-1
focus:outline-none
focus:outline-primary
focus:ring-0
font-bold
font-inter
font-medium
font-normal
font-semibold
gap-0.5
gap-1
gap-1.5
gap-2
gap-3
gap-4
gap-5
gap-6
gap-x-1
gap-x-2
gap-x-3
gap-y-1
gap-y-3
grid
grid-cols-1
grid-flow-col
grow
grow-0
h-12
h-2
h-32
h-64
h-8
h-auto
h-full
h-screen
helper-text
hidden
hover:bg-primary/5
hover:bg-transparent
hover:text-base-content
hover:text-base-content/60
hover:text-primary
image-full
inline
inline-block
inline-flex
inline-grid
input
input-floating
input-floating-label
input-lg
input-md
input-sm
input-xl
inset-0
inset-y-0
inset-y-2
invisible
is-invalid
is-valid
isolate
italic
items-center
items-end
items-start
join
join-item
justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
left-0
lg:bg-base-100/20
lg:flex
lg:gap-y-0
lg:grid-cols-2
lg:hidden
lg:justify-end
lg:justify-start
lg:min-w-0
lg:p-10
lg:pb-5
lg:pl-64
lg:pr-3.5
lg:pt-5
lg:ring-1
lg:ring-base-content/10
lg:rounded-lg
lg:shadow-xs
list-disc
list-inside
loading
loading-lg
loading-sm
loading-spinner
loading-xl
loading-xs
lowercase
m-10
mask
max-lg:flex-col
max-lg:hidden
max-w-7xl
max-w-80
max-w-full
max-w-lg
max-w-sm
max-w-xl
mb-0.5
mb-1
mb-2
mb-3
mb-4
mb-5
md:table-cell
md:text-3xl
me-1
me-1.5
me-2
me-5
menu
menu-active
menu-disabled
menu-dropdown
menu-dropdown-show
menu-focus
menu-horizontal
menu-title
min-h-0
min-h-svh
min-w-0
min-w-28
min-w-48
min-w-60
min-w-[100px]
ml-3
ml-4
modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-1
ms-2
ms-3
mt-1
mt-1.5
mt-10
mt-12
mt-2
mt-2.5
mt-3
mt-4
mt-5
mt-8
mx-1
mx-auto
my-3
my-auto
opacity-0
opacity-100
open
origin-top-left
outline
outline-0
overflow-hidden
overflow-x-auto
overflow-y-auto
overlay-open:duration-50
overlay-open:opacity-100
p-0.5
p-1
p-2
p-3
p-4
p-5
p-6
p-8
pin-input
pin-input-underline
placeholder-base-content/60
pointer-events-auto
pointer-events-none
progress
progress-bar
progress-indeterminate
progress-primary
pt-0.5
pt-3
px-1.5
px-2
px-3
px-4
px-5
py-0.5
py-1.5
py-2
py-2.5
py-6
radio
range
relative
resize
ring-0
ring-1
ring-white/10
rounded-box
rounded-field
rounded-full
rounded-lg
rounded-md
rounded-t-lg
row-active
row-hover
rtl:!mr-0
select
select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
selected
selected:select-active
shadow-base-300/20
shadow-lg
shadow-xs
shrink-0
size-10
size-4
size-5
size-8
skeleton
skeleton-animated
sm:auto-cols-max
sm:flex
sm:items-center
sm:items-end
sm:justify-between
sm:justify-end
sm:max-w-2xl
sm:max-w-3xl
sm:max-w-4xl
sm:max-w-5xl
sm:max-w-6xl
sm:max-w-7xl
sm:max-w-lg
sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-y-1
space-y-4
sr-only
static
status
status-error
sticky
switch
tab
tab-active
table
table-pin-cols
table-pin-rows
tabs
tabs-bordered
tabs-lg
tabs-lifted
tabs-md
tabs-sm
tabs-xl
tabs-xs
text-2xl
text-4xl
text-accent
text-base
text-base-content
text-base-content/40
text-base-content/50
text-base-content/60
text-base-content/70
text-base-content/80
text-base/6
text-center
text-error
text-error-content
text-gray-400
text-info
text-info-content
text-left
text-lg
text-primary
text-primary-content
text-sm
text-sm/5
text-success
text-success-content
text-warning
text-warning-content
text-xl
text-xs
text-xs/5
textarea
textarea-floating
textarea-floating-label
theme-controller
tooltip
tooltip-content
top-0
top-1/2
top-full
transform
transition
transition-all
transition-opacity
translate-x-0
truncate
underline
uppercase
validate
w-0
w-0.5
w-12
w-4
w-56
w-64
w-fit
w-full
whitespace-nowrap
z-10
z-40
z-50

View File

@@ -1,11 +1,11 @@
<div class="card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-slate-200">
<p class="text-xl font-semibold text-base-content">
@Text
</p>
<i class="@Icon text-4xl text-primary"></i>
</div>
<p class="text-base text-slate-300">@Title</p>
<p class="text-base-content/80">@Title</p>
</div>
@code

View File

@@ -17,7 +17,7 @@
<i class="icon-palette mix-blend-exclusion"></i>
<span class="ms-2 mix-blend-exclusion">@(currentHex)</span>
</label>
<button @onclick="Reset" class="btn btn-danger">
<button @onclick="Reset" class="btn btn-error">
<i class="icon-rotate-ccw"></i>
</button>
</div>

View File

@@ -5,7 +5,7 @@
<div class="animate-shimmer bg-gradient-to-r from-violet-400 via-sky-400 to-purple-400 bg-clip-text font-semibold text-transparent text-3xl" style="animation-duration: 5s; background-size: 200% 100%">
Welcome, @(Username)
</div>
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
<div class="text-base-content/90 text-2xl">What do you want to do today?</div>
</div>
</div>

View File

@@ -1,13 +1,14 @@
@using Moonlight.Client.UI.Partials
@using MoonCore.Blazor.FlyonUi.Files.Drop
@inherits LayoutComponentBase
<div class="relative isolate flex min-h-svh w-full h-screen max-lg:flex-col bg-gray-900 lg:bg-gray-950/80">
<div class="relative isolate flex min-h-svh w-full h-screen max-lg:flex-col bg-background">
<AppSidebar Layout="this"/>
<AppHeader Layout="this" />
<main class="h-full flex flex-1 flex-col lg:pb-5 lg:min-w-0 lg:pt-5 lg:pr-3.5 lg:pl-64">
<div class="h-full grow p-6 lg:rounded-lg lg:p-10 lg:ring-1 lg:shadow-xs lg:bg-gray-900/80 lg:ring-white/10">
<div class="grow p-6 lg:rounded-lg lg:p-10 lg:ring-1 lg:shadow-xs lg:bg-base-100/20 lg:ring-base-content/10">
<div class="h-full mx-auto max-w-7xl">
<CascadingValue Value="this" IsFixed="true">
@Body
@@ -15,20 +16,21 @@
</div>
</div>
</main>
</div>
<ToastLauncher/>
<ModalLauncher/>
<DropHandler />
<div id="blazor-error-ui" class="fixed bottom-0 left-0 w-full z-50">
<div class="bg-error text-white p-4 flex flex-row justify-between items-center">
<div class="bg-error text-base-content p-4 flex flex-row justify-between items-center">
<div class="flex items-center">
<i class="icon-bomb text-lg text-white me-2"></i>
<i class="icon-bomb text-lg text-base-content me-2"></i>
<span>An unhandled error has occurred.</span>
</div>
<div>
<a href="#" class="reload text-white underline mr-4">Reload</a>
<a href="#" class="reload text-base-content underline mr-4">Reload</a>
<a href="#" class="dismiss hidden">🗙</a>
</div>
</div>

View File

@@ -2,11 +2,11 @@
@inject NavigationManager Navigation
<header class="flex items-center px-4 lg:hidden border-b border-white/5">
<header class="flex items-center px-4 lg:hidden border-b border-base-content/5">
<div class="py-2.5">
<span class="relative">
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Open navigation"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-white"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-base-content"
type="button">
<i class="icon-menu text-xl"></i>
</button>
@@ -18,12 +18,12 @@
</div>
<div class="flex items-center gap-3">
<span class="relative">
<div class="relative flex min-w-0 cursor-default items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium sm:text-sm/5 text-white">
<div class="relative flex min-w-0 cursor-default items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium sm:text-sm/5 text-base-content">
<div data-slot="avatar"
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full"
src="/img/pfp_placeholder.png"
src="/svg/logo.svg"
alt=""/>
</div>
</div>

View File

@@ -17,17 +17,17 @@
<div class="fixed inset-y-0 left-0 w-64 max-lg:hidden">
<nav class="flex h-full min-h-0 flex-col">
<div class="flex flex-col border-b p-4 border-white/5">
<div class="flex flex-col border-b p-4 border-base-content/5">
<span class="relative">
<div type="button"
class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-lg font-medium text-gray-100">
class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-lg font-medium text-base-content">
<span
class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
src="/svg/logo.svg"
alt=""/>
</span>
<span class="truncate">Moonlight</span>
<span class="truncate">Moonlight v2.1</span>
</div>
</span>
</div>
@@ -37,7 +37,7 @@
{
if (!string.IsNullOrEmpty(item.Key))
{
<h3 class="mt-4 px-2 text-sm/5 font-medium text-gray-400">
<h3 class="mt-4 px-2 text-sm/5 font-medium text-base-content/40">
@item.Key
</h3>
}
@@ -51,10 +51,10 @@
@if (isMatch)
{
<div class="relative">
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-white"
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-primary"
style="opacity: 1;">
</span>
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-white/5 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-primary/5 sm:py-2 sm:text-sm/5 text-base-content"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -66,7 +66,7 @@
else
{
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-white hover:bg-white/5"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-base-content hover:bg-primary/5"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -79,9 +79,9 @@
}
</div>
</div>
<div class="flex flex-col border-t p-4 max-lg:hidden border-white/5 mt-2.5">
<div class="flex flex-col border-t p-4 max-lg:hidden border-base-content/5 mt-2.5">
<div
class="flex w-full items-center px-2 py-2.5 gap-6 rounded-lg text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-white">
class="flex w-full items-center px-2 py-2.5 gap-6 rounded-lg text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-base-content">
<div class="flex min-w-0 items-center gap-3">
<span class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
@@ -89,10 +89,10 @@
alt=""/>
</span>
<div class="min-w-0">
<div class="block truncate text-sm/5 font-medium text-white">
<div class="block truncate text-sm/5 font-medium text-base-content">
@Username
</div>
<div class="block truncate text-xs/5 font-normal text-gray-400">
<div class="block truncate text-xs/5 font-normal text-base-content/40">
@Email
</div>
</div>
@@ -108,22 +108,23 @@
<div
class="lg:hidden z-50 transition-opacity ease-linear duration-300 @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 pointer-events-none")"
role="dialog" tabindex="-1">
<div class="fixed inset-0 bg-black/30"></div>
<div class="fixed inset-0 bg-background/60"></div>
<div class="fixed inset-y-0 w-full max-w-80 p-2">
<div
class="relative flex h-full flex-col rounded-lg shadow-xs ring-1 bg-gray-900 ring-white/10 transition ease-in-out duration-300 transform @(Layout.ShowMobileNavigation ? "translate-x-0" : "-translate-x-full")">
<div class="border-b p-4 border-white/5 flex justify-between px-5 pt-3">
<div class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-white">
class="relative flex h-full flex-col rounded-lg shadow-xs ring-1 bg-base-300 ring-white/10 transition ease-in-out duration-300 transform @(Layout.ShowMobileNavigation ? "translate-x-0" : "-translate-x-full")">
<div class="border-b p-4 border-base-content/5 flex justify-between px-5 pt-3">
<div
class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-base-content">
<div data-slot="avatar"
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full" src="/svg/logo.svg" alt=""/>
class="h-8 rounded-full" src="/placeholder.jpg" alt=""/>
</div>
<div class="truncate">Moonlight</div>
<div class="truncate">Moonlight v2.1</div>
</div>
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Close navigation" type="button"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-white">
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-base-content">
<i class="icon-x text-lg"></i>
</button>
</div>
@@ -149,10 +150,10 @@
@if (isMatch)
{
<div class="relative">
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-white"
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-primary"
style="opacity: 1;">
</span>
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-white/5 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-primary/5 sm:py-2 sm:text-sm/5 text-base-content"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -164,7 +165,7 @@
else
{
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-white hover:bg-white/5"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-base-content hover:bg-primary/5"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -180,8 +181,7 @@
<div class="mt-8 flex-1"></div>
<div class="flex flex-col gap-0.5">
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-base-content"
href="#" @onclick:preventDefault @onclick="Logout">
<i class="icon-log-out"></i>
<span class="truncate">Logout</span>

View File

@@ -4,22 +4,22 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using MoonCore.Blazor.Tailwind.Input2
@using MoonCore.Blazor.FlyonUi.Forms
@using MoonCore.Blazor.FlyonUi.Helpers
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@inject ToastService ToastService
@inject AlertService AlertService
@* @inject DownloadService DownloadService *@
@inject DownloadService DownloadService
<PageHeader Title="Create API Key">
<a href="/admin/api" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>
@@ -28,21 +28,21 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Description</label>
<label class="block text-sm font-medium leading-6 text-base-content">Description</label>
<div class="mt-2">
<input @bind="Request.Description" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Description" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Expires at</label>
<label class="block text-sm font-medium leading-6 text-base-content">Expires at</label>
<div class="mt-2">
<input @bind="Request.ExpiresAt" type="date" autocomplete="off" class="form-input w-full">
<input @bind="Request.ExpiresAt" type="date" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -54,7 +54,7 @@
private HandleForm Form;
private CreateApiKeyRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
protected override void OnInitialized()
{
@@ -63,12 +63,12 @@
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
Request.ExpiresAt = Request.ExpiresAt.ToUniversalTime();
var response = await ApiClient.PostJson<CreateApiKeyResponse>("api/admin/apikeys", Request);
await DownloadService.DownloadString(
await DownloadService.Download(
$"moonlight-key-{response.Id}.txt",
response.Secret
);

View File

@@ -23,7 +23,7 @@
</div>
<div class="col-span-1 card card-body border-l-4 border-tertiary">
<p class="font-medium tracking-wide text-slate-100">
<p class="font-medium tracking-wide text-base-content">
Learn about the api usage
</p>
<a href="https://help.moonlightpanel.xyz" target="_blank" class="mt-2 flex items-center justify-between text-primary">
@@ -35,7 +35,7 @@
</div>
<div class="col-span-1 card card-body border-l-4 border-success">
<p class="font-medium tracking-wide text-slate-100">
<p class="font-medium tracking-wide text-base-content">
Open API Specification
</p>
<a href="/api/swagger/main" target="_blank" class="mt-2 flex items-center justify-between text-primary">
@@ -61,7 +61,7 @@
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Description)" Name="Description"/>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.ExpiresAt)" Name="Expires at">
<ColumnTemplate>
@(Formatter.FormatDate(context.ExpiresAt))
@(Formatter.FormatDate(context.ExpiresAt.UtcDateTime))
</ColumnTemplate>
</DataTableColumn>
<DataTableColumn TItem="ApiKeyResponse">
@@ -72,7 +72,7 @@
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-danger">
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>

View File

@@ -12,11 +12,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update API Key">
<a href="/admin/api" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -25,9 +25,9 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Description</label>
<label class="block text-sm font-medium leading-6 text-base-content">Description</label>
<div class="mt-2">
<input @bind="Request.Description" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Description" type="text" autocomplete="off" class="input w-full">
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
@page "/admin/system/advanced"
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers
@attribute [Authorize(Policy = "permissions:admin.system.advanced")]
@@ -36,6 +37,6 @@
{
var stream = await ApiClient.GetStream("api/admin/system/advanced/frontend");
await DownloadService.DownloadStream("frontend.zip", stream);
await DownloadService.Download("frontend.zip", stream);
}
}

View File

@@ -1,6 +1,7 @@
@page "/admin/system/diagnose"
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Sys
@using Moonlight.Shared.Http.Responses.Admin.Sys
@@ -47,7 +48,7 @@
<div class="@(DropdownOpen ? "" : "hidden")">
<LazyLoader Load="Load">
<div class="mb-2 py-2 border-b border-gray-700 flex items-center gap-3">
<div class="mb-2 py-2 border-b border-base-content/70 flex items-center gap-3">
<input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="form-checkbox">
<label for="selectall_checkbox">Select all</label>
</div>
@@ -99,7 +100,7 @@
if (!SelectAll)
{
// filter the providers which have been selected if not all providers have been selected
// Filter the providers which have been selected if not all providers have been selected
request.Providers = AvailableProviders
.Where(x => x.Value)
.Select(x => x.Key.Type)
@@ -108,7 +109,7 @@
var stream = await ApiClient.PostStream("api/admin/system/diagnose", request);
await DownloadService.DownloadStream("diagnose.zip", stream);
await DownloadService.Download("diagnose.zip", stream);
}

View File

@@ -3,31 +3,28 @@
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.Services
@using MoonCore.Helpers
@using MoonCore.Blazor.Tailwind.Fm
@using Moonlight.Client.Implementations
@using MoonCore.Blazor.FlyonUi.Files.Manager
@attribute [Authorize(Policy = "permissions:admin.system.overview")]
@inject HttpApiClient ApiClient
@inject DownloadService DownloadService
@inject LocalStorageService LocalStorageService
<div class="mb-5">
<NavTabs Index="2" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
</div>
<FileManager FileSystemProvider="FileSystemProvider" MaxUploadSize="4096"/>
<FileManager FsAccess="FsAccess" TransferChunkSize="TransferChunkSize" UploadLimit="UploadLimit"/>
@code
{
private IFileSystemProvider FileSystemProvider;
private IFsAccess FsAccess;
private static readonly long TransferChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
private static readonly long UploadLimit = ByteConverter.FromGigaBytes(20).Bytes;
protected override void OnInitialized()
{
FileSystemProvider = new SysFileSystemProvider(
ApiClient,
DownloadService,
LocalStorageService
);
FsAccess = new SystemFsAccess(ApiClient);
}
}

View File

@@ -22,8 +22,8 @@
<div class="card card-body">
<div class="flex justify-center">
<WButton OnClick="Restart" CssClasses="btn btn-danger w-full">
<i class="icon-repeat text-xl text-white me-2"></i>
<WButton OnClick="Restart" CssClasses="btn btn-error w-full">
<i class="icon-repeat text-xl text-base-content me-2"></i>
Restart/Shutdown
</WButton>
</div>

View File

@@ -3,6 +3,7 @@
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -10,11 +11,11 @@
<PageHeader Title="Create User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>
@@ -23,27 +24,27 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Username</label>
<label class="block text-sm font-medium leading-6 text-base-content">Username</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Username" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Email</label>
<label class="block text-sm font-medium leading-6 text-base-content">Email</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
<input @bind="Request.Email" type="email" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Password</label>
<label class="block text-sm font-medium leading-6 text-base-content">Password</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
<input @bind="Request.Password" type="password" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -55,7 +56,7 @@
private HandleForm Form;
private CreateUserRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
protected override void OnInitialized()
{
@@ -64,7 +65,7 @@
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
await ApiClient.Post("api/admin/users", Request);

View File

@@ -32,7 +32,7 @@
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-danger">
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>

View File

@@ -4,6 +4,7 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -12,11 +13,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -25,27 +26,27 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Username</label>
<label class="block text-sm font-medium leading-6 text-base-content">Username</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Username" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Email</label>
<label class="block text-sm font-medium leading-6 text-base-content">Email</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
<input @bind="Request.Email" type="email" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Password</label>
<label class="block text-sm font-medium leading-6 text-base-content">Password</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
<input @bind="Request.Password" type="password" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -60,25 +61,25 @@
private HandleForm Form;
private UpdateUserRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
private async Task Load(LazyLoader _)
{
var detail = await ApiClient.GetJson<UserResponse>($"api/admin/users/{Id}");
Permissions = JsonSerializer.Deserialize<string[]>(detail.PermissionsJson) ?? [];
Permissions = detail.Permissions.ToList();
Request = new()
{
Email = detail.Email,
PermissionsJson = detail.PermissionsJson,
Permissions = detail.Permissions,
Username = detail.Username
};
}
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
await ApiClient.Patch($"api/admin/users/{Id}", Request);

View File

@@ -1,316 +0,0 @@
window.moonCore = {
window: {
getSize: function () {
return [window.innerWidth, window.innerHeight];
}
},
keyBinds: {
storage: {},
registerHotkey: function (key, modifier, action, dotNetObjRef) {
const hotkeyListener = async (event) => {
if (event.code === key && (!modifier || event[modifier + 'Key'])) {
event.preventDefault();
await dotNetObjRef.invokeMethodAsync("OnHotkeyPressed", action);
}
};
moonCore.keyBinds.storage[`${key}${modifier}`] = hotkeyListener;
window.addEventListener('keydown', hotkeyListener);
},
unregisterHotkey: function (key, modifier) {
const listenerKey = `${key}${modifier}`;
if (moonCore.keyBinds.storage[listenerKey]) {
window.removeEventListener('keydown', moonCore.keyBinds.storage[listenerKey]);
delete moonCore.keyBinds.storage[listenerKey];
}
}
},
downloadService: {
download: async function (fileName, contentStreamReference, id, reportRef) {
const promise = new Promise(async resolve => {
const stream = await contentStreamReference.stream();
const reader = stream.getReader();
let lastReportTime = 0;
let receivedLength = 0; // Track downloaded size
let chunks = []; // Store downloaded chunks
while (true) {
const {done, value} = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (reportRef) {
const now = Date.now();
if (now - lastReportTime >= 500) { // Only log once per second
await reportRef.invokeMethodAsync("ReceiveReport", id, receivedLength, -1, false);
lastReportTime = now;
}
}
}
// Combine chunks into a single Blob
const blob = new Blob(chunks);
this.downloadBlob(fileName, blob);
if (reportRef)
await reportRef.invokeMethodAsync("ReceiveReport", id, receivedLength, -1, true);
resolve();
});
await promise;
},
downloadUrl: async function (fileName, url, reportRef, id, headers) {
const promise = new Promise(async resolve => {
let loadRequest = new XMLHttpRequest();
let lastReported = Date.now();
loadRequest.open("GET", url, true);
for(let headerKey in headers) {
loadRequest.setRequestHeader(headerKey, headers[headerKey]);
}
loadRequest.responseType = "blob";
if (reportRef) {
loadRequest.onprogress = async ev => {
const now = Date.now();
if (now - lastReported >= 500) {
await reportRef.invokeMethodAsync("ReceiveReport", id, ev.loaded, ev.total, false);
lastReported = now;
}
};
loadRequest.onloadend = async ev => {
await reportRef.invokeMethodAsync("ReceiveReport", id, ev.loaded, ev.total, true);
}
}
loadRequest.onload = _ => {
this.downloadBlob(fileName, loadRequest.response);
resolve();
}
loadRequest.send();
});
await promise;
},
downloadBlob: function (fileName, blob)
{
const url = URL.createObjectURL(blob);
// Trigger file download
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}
},
fileManager: {
uploadCache: [],
addFilesToCache: async function(id) {
let files = document.getElementById(id).files;
for (let i = 0; i < files.length; i++) {
moonCore.fileManager.uploadCache.push(files[i]);
}
await this.ref.invokeMethodAsync("TriggerUpload", moonCore.fileManager.uploadCache.length);
},
getNextFromCache: async function() {
if(moonCore.fileManager.uploadCache.length === 0)
return null;
let nextItem = moonCore.fileManager.uploadCache.pop();
if(!nextItem)
return null;
let file;
let path;
if(nextItem instanceof File)
{
file = nextItem;
path = file.name;
}
else
{
file = await this.openFileEntry(nextItem);
path = nextItem.fullPath;
}
if(file.size === 0)
{
return {
path: null,
stream: null,
left: moonCore.fileManager.uploadCache.length
}
}
let stream = await this.createStreamRef(file);
return {
path: path,
stream: stream,
left: moonCore.fileManager.uploadCache.length
};
},
openFileEntry: async function (fileEntry) {
const promise = new Promise(resolve => {
fileEntry.file(file => {
resolve(file);
}, err => console.log(err));
});
return await promise;
},
createStreamRef: async function (processedFile) {
// Prevent uploads of empty files
if (processedFile.size <= 0) {
console.log("Skipping upload of '" + processedFile.name + "' as its empty");
return null;
}
const fileReader = new FileReader();
const readerPromise = new Promise(resolve => {
fileReader.addEventListener("loadend", ev => {
resolve(fileReader.result)
});
});
fileReader.readAsArrayBuffer(processedFile);
const arrayBuffer = await readerPromise;
return DotNet.createJSStreamReference(arrayBuffer);
},
setup: function (id, callbackRef) {
this.ref = callbackRef;
// Check which features are supported by the browser
const supportsFileSystemAccessAPI =
'getAsFileSystemHandle' in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
'webkitGetAsEntry' in DataTransferItem.prototype;
// This is the drag and drop zone.
const elem = document.getElementById(id);
// Prevent navigation.
elem.addEventListener('dragover', (e) => {
e.preventDefault();
});
elem.addEventListener('drop', async (e) => {
// Prevent navigation.
e.preventDefault();
if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
// Cannot handle directories.
console.log("Cannot handle directories");
return;
}
this.getAllWebkitFileEntries(e.dataTransfer.items).then(async value => {
value.forEach(a => moonCore.fileManager.uploadCache.push(a));
await this.ref.invokeMethodAsync("TriggerUpload", this.uploadCache.length);
});
});
},
getAllWebkitFileEntries: async function (dataTransferItemList) {
function readAllEntries(reader) {
return new Promise((resolve, reject) => {
const entries = [];
function readEntries() {
reader.readEntries((batch) => {
if (batch.length === 0) {
resolve(entries);
} else {
entries.push(...batch);
readEntries();
}
}, reject);
}
readEntries();
});
}
async function traverseEntry(entry) {
if (entry.isFile) {
return [entry];
} else if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await readAllEntries(reader);
const subEntries = await Promise.all(entries.map(traverseEntry));
return subEntries.flat();
}
return [];
}
const entries = [];
// Convert DataTransferItemList to entries
for (let i = 0; i < dataTransferItemList.length; i++) {
const item = dataTransferItemList[i];
const entry = item.webkitGetAsEntry();
if (entry) {
entries.push(entry);
}
}
// Traverse all entries and collect file entries
const allFileEntries = await Promise.all(entries.map(traverseEntry));
return allFileEntries.flat();
}
},
codeEditor: {
instances: new Map(),
attach: function (id, options) {
const editor = ace.edit(id, options);
moonCore.codeEditor.instances.set(id, editor);
},
updateOptions: function (id, options) {
const editor = moonCore.codeEditor.instances.get(id);
editor.setOptions(options);
},
getValue: function (id) {
const editor = moonCore.codeEditor.instances.get(id);
return editor.getValue();
},
destroy: function (id){
const editor = moonCore.codeEditor.instances.get(id);
if(!editor)
return;
editor.destroy();
editor.container.remove();
moonCore.codeEditor.instances.delete(id);
}
}
}

View File

@@ -8,7 +8,7 @@ public class CreateApiKeyRequest
public string Description { get; set; }
[Required(ErrorMessage = "You need to specify permissions for the api key")]
public string PermissionsJson { get; set; } = "[]";
public string[] Permissions { get; set; } = [];
[Required(ErrorMessage = "You need to specify an expire date")]
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddDays(30);

View File

@@ -17,5 +17,5 @@ public class CreateUserRequest
[MaxLength(256, ErrorMessage = "Your password should not exceed the length of 256 characters")]
public string Password { get; set; }
public string PermissionsJson { get; set; } = "[]";
public string[] Permissions { get; set; } = [];
}

View File

@@ -15,5 +15,5 @@ public class UpdateUserRequest
public string? Password { get; set; }
[Required(ErrorMessage = "You need to provide permissions")]
public string PermissionsJson { get; set; } = "[]";
public string[] Permissions { get; set; } = [];
}

View File

@@ -4,6 +4,6 @@ public class ApiKeyResponse
{
public int Id { get; set; }
public string Description { get; set; }
public string PermissionsJson { get; set; } = "[]";
public DateTime ExpiresAt { get; set; }
public string[] Permissions { get; set; } = [];
public DateTimeOffset ExpiresAt { get; set; }
}

View File

@@ -5,6 +5,6 @@ public class CreateApiKeyResponse
public int Id { get; set; }
public string Secret { get; set; }
public string Description { get; set; }
public string PermissionsJson { get; set; } = "[]";
public DateTime ExpiresAt { get; set; }
public string[] Permissions { get; set; } = [];
public DateTimeOffset ExpiresAt { get; set; }
}

View File

@@ -3,7 +3,7 @@
public class FileSystemEntryResponse
{
public string Name { get; set; }
public bool IsFile { get; set; }
public bool IsFolder { get; set; }
public long Size { get; set; }
public DateTime CreatedAt { get; set; }

View File

@@ -5,5 +5,5 @@ public class UserResponse
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string PermissionsJson { get; set; }
public string[] Permissions { get; set; }
}

View File

@@ -4,5 +4,5 @@ public class CheckResponse
{
public string Username { get; set; }
public string Email { get; set; }
public string Permissions { get; set; }
public string[] Permissions { get; set; }
}