Merge branch 'main' into DiscordBot

This commit is contained in:
Spielepapagei 2023-04-04 01:20:12 +02:00
commit 0da6577f43
123 changed files with 10387 additions and 1389 deletions

View file

@ -2,6 +2,7 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.Database;
@ -34,14 +35,13 @@ public class DataContext : DbContext
public DbSet<SharedDomain> SharedDomains { get; set; }
public DbSet<Domain> Domains { get; set; }
public DbSet<Subscription> Subscriptions { get; set; }
public DbSet<SubscriptionLimit> SubscriptionLimits { get; set; }
public DbSet<Revoke> Revokes { get; set; }
public DbSet<NotificationClient> NotificationClients { get; set; }
public DbSet<NotificationAction> NotificationActions { get; set; }
public DbSet<AaPanel> AaPanels { get; set; }
public DbSet<Website> Websites { get; set; }
public DbSet<DdosAttack> DdosAttacks { get; set; }
public DbSet<Subscription> Subscriptions { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View file

@ -1,8 +1,12 @@
namespace Moonlight.App.Database.Entities;
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Database.Entities;
public class Domain
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public SharedDomain SharedDomain { get; set; }
public User Owner { get; set; }

View file

@ -20,4 +20,5 @@ public class Server
public NodeAllocation MainAllocation { get; set; } = null!;
public Node Node { get; set; } = null!;
public User Owner { get; set; } = null!;
public bool IsCleanupException { get; set; } = false;
}

View file

@ -5,7 +5,5 @@ public class Subscription
public int Id { get; set; }
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string SellPassId { get; set; } = "";
public int Duration { get; set; }
public List<SubscriptionLimit> Limits { get; set; } = new();
public string LimitsJson { get; set; } = "";
}

View file

@ -1,11 +0,0 @@
namespace Moonlight.App.Database.Entities;
public class SubscriptionLimit
{
public int Id { get; set; }
public Image Image { get; set; } = null!;
public int Amount { get; set; }
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
}

View file

@ -1,4 +1,5 @@
using Moonlight.App.Models.Misc;
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities;
@ -7,14 +8,21 @@ public class User
public int Id { get; set; }
// Personal data
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string Address { get; set; } = "";
public string City { get; set; } = "";
public string State { get; set; } = "";
public string Country { get; set; } = "";
// States
@ -34,9 +42,10 @@ public class User
// Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Subscriptions
public Subscription? Subscription { get; set; } = null;
public DateTime? SubscriptionSince { get; set; }
public Subscription? CurrentSubscription { get; set; } = null;
public DateTime SubscriptionSince { get; set; } = DateTime.Now;
public int SubscriptionDuration { get; set; }
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,170 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddCleanupExceptionsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "State",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Country",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Users",
type: "varchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Users",
type: "varchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "CleanupExceptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServerId = table.Column<int>(type: "int", nullable: false),
Note = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_CleanupExceptions", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CleanupExceptions");
migrationBuilder.AlterColumn<string>(
name: "State",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Country",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(128)",
oldMaxLength: 128)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(128)",
oldMaxLength: 128)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class ChengedCleanupExceptionModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "CleanupExceptions",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateIndex(
name: "IX_CleanupExceptions_ServerId",
table: "CleanupExceptions",
column: "ServerId");
migrationBuilder.AddForeignKey(
name: "FK_CleanupExceptions_Servers_ServerId",
table: "CleanupExceptions",
column: "ServerId",
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_CleanupExceptions_Servers_ServerId",
table: "CleanupExceptions");
migrationBuilder.DropIndex(
name: "IX_CleanupExceptions_ServerId",
table: "CleanupExceptions");
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "CleanupExceptions");
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class RemovedCleanupExceptionChangedServerModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CleanupExceptions");
migrationBuilder.AddColumn<bool>(
name: "IsCleanupException",
table: "Servers",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsCleanupException",
table: "Servers");
migrationBuilder.CreateTable(
name: "CleanupExceptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServerId = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Note = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_CleanupExceptions", x => x.Id);
table.ForeignKey(
name: "FK_CleanupExceptions_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_CleanupExceptions_ServerId",
table: "CleanupExceptions",
column: "ServerId");
}
}
}

View file

@ -0,0 +1,962 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.App.Database;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20230403130734_RemovedOldSubscriptionData")]
partial class RemovedOldSubscriptionData
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Moonlight.App.Database.Entities.AaPanel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("BaseDomain")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AaPanels");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("AaPanelId")
.HasColumnType("int");
b.Property<int>("InternalAaPanelId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AaPanelId");
b.HasIndex("OwnerId");
b.ToTable("Databases");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("Data")
.HasColumnType("bigint");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("NodeId")
.HasColumnType("int");
b.Property<bool>("Ongoing")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.HasIndex("NodeId");
b.ToTable("DdosAttacks");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("Default")
.HasColumnType("tinyint(1)");
b.Property<int?>("ImageId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ImageId");
b.ToTable("DockerImages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<int>("SharedDomainId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.HasIndex("SharedDomainId");
b.ToTable("Domains");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Allocations")
.HasColumnType("int");
b.Property<string>("ConfigFiles")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("InstallDockerImage")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("InstallEntrypoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("InstallScript")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Startup")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StartupDetection")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("TagsJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("Uuid")
.HasColumnType("char(36)");
b.HasKey("Id");
b.ToTable("Images");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ImageTag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("ImageTags");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("ImageId")
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ImageId");
b.ToTable("ImageVariables");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("LoadingMessages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("AuditLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Class")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Stacktrace")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("ErrorLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SecurityLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("HttpPort")
.HasColumnType("int");
b.Property<int>("MoonlightDaemonPort")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("SftpPort")
.HasColumnType("int");
b.Property<bool>("Ssl")
.HasColumnType("tinyint(1)");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("TokenId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Nodes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("NodeId")
.HasColumnType("int");
b.Property<int>("Port")
.HasColumnType("int");
b.Property<int?>("ServerId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("ServerId");
b.ToTable("NodeAllocations");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Action")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("NotificationClientId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("NotificationClientId");
b.ToTable("NotificationActions");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationClients");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Revokes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Cpu")
.HasColumnType("int");
b.Property<long>("Disk")
.HasColumnType("bigint");
b.Property<int>("DockerImageIndex")
.HasColumnType("int");
b.Property<int>("ImageId")
.HasColumnType("int");
b.Property<bool>("Installing")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsCleanupException")
.HasColumnType("tinyint(1)");
b.Property<int>("MainAllocationId")
.HasColumnType("int");
b.Property<long>("Memory")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("NodeId")
.HasColumnType("int");
b.Property<string>("OverrideStartup")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<bool>("Suspended")
.HasColumnType("tinyint(1)");
b.Property<Guid>("Uuid")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("ImageId");
b.HasIndex("MainAllocationId");
b.HasIndex("NodeId");
b.HasIndex("OwnerId");
b.ToTable("Servers");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<long>("Bytes")
.HasColumnType("bigint");
b.Property<bool>("Created")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("ServerId")
.HasColumnType("int");
b.Property<Guid>("Uuid")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerBackups");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("ServerId")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerVariables");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CloudflareId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("SharedDomains");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsQuestion")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSupport")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("RecipientId")
.HasColumnType("int");
b.Property<int?>("SenderId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("RecipientId");
b.HasIndex("SenderId");
b.ToTable("SupportMessages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("Admin")
.HasColumnType("tinyint(1)");
b.Property<string>("City")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("DiscordId")
.HasColumnType("bigint");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("State")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<bool>("SupportPending")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("TokenValidTime")
.HasColumnType("datetime(6)");
b.Property<bool>("TotpEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("TotpSecret")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("AaPanelId")
.HasColumnType("int");
b.Property<string>("DomainName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("FtpPassword")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("FtpUsername")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("InternalAaPanelId")
.HasColumnType("int");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<string>("PhpVersion")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("AaPanelId");
b.HasIndex("OwnerId");
b.ToTable("Websites");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b =>
{
b.HasOne("Moonlight.App.Database.Entities.AaPanel", "AaPanel")
.WithMany()
.HasForeignKey("AaPanelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AaPanel");
b.Navigation("Owner");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
.WithMany()
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Image", null)
.WithMany("DockerImages")
.HasForeignKey("ImageId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain")
.WithMany()
.HasForeignKey("SharedDomainId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
b.Navigation("SharedDomain");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Image", null)
.WithMany("Variables")
.HasForeignKey("ImageId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Node", null)
.WithMany("Allocations")
.HasForeignKey("NodeId");
b.HasOne("Moonlight.App.Database.Entities.Server", null)
.WithMany("Allocations")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient")
.WithMany()
.HasForeignKey("NotificationClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("NotificationClient");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Image", "Image")
.WithMany()
.HasForeignKey("ImageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation")
.WithMany()
.HasForeignKey("MainAllocationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
.WithMany()
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Image");
b.Navigation("MainAllocation");
b.Navigation("Node");
b.Navigation("Owner");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Server", null)
.WithMany("Backups")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Server", null)
.WithMany("Variables")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
.WithMany()
.HasForeignKey("RecipientId");
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
.WithMany()
.HasForeignKey("SenderId");
b.Navigation("Recipient");
b.Navigation("Sender");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
{
b.HasOne("Moonlight.App.Database.Entities.AaPanel", "AaPanel")
.WithMany()
.HasForeignKey("AaPanelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AaPanel");
b.Navigation("Owner");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
{
b.Navigation("Allocations");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{
b.Navigation("Allocations");
b.Navigation("Backups");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,269 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class RemovedOldSubscriptionData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Users_Subscriptions_SubscriptionId",
table: "Users");
migrationBuilder.DropTable(
name: "SubscriptionLimits");
migrationBuilder.DropTable(
name: "Subscriptions");
migrationBuilder.DropIndex(
name: "IX_Users_SubscriptionId",
table: "Users");
migrationBuilder.DropColumn(
name: "SubscriptionDuration",
table: "Users");
migrationBuilder.DropColumn(
name: "SubscriptionId",
table: "Users");
migrationBuilder.DropColumn(
name: "SubscriptionSince",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "State",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Country",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(128)",
oldMaxLength: 128)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Users",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(128)",
oldMaxLength: 128)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "State",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Country",
table: "Users",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Users",
type: "varchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Users",
type: "varchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<int>(
name: "SubscriptionDuration",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "SubscriptionId",
table: "Users",
type: "int",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "SubscriptionSince",
table: "Users",
type: "datetime(6)",
nullable: true);
migrationBuilder.CreateTable(
name: "Subscriptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Duration = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SellPassId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Subscriptions", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "SubscriptionLimits",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ImageId = table.Column<int>(type: "int", nullable: false),
Amount = table.Column<int>(type: "int", nullable: false),
Cpu = table.Column<int>(type: "int", nullable: false),
Disk = table.Column<int>(type: "int", nullable: false),
Memory = table.Column<int>(type: "int", nullable: false),
SubscriptionId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SubscriptionLimits", x => x.Id);
table.ForeignKey(
name: "FK_SubscriptionLimits_Images_ImageId",
column: x => x.ImageId,
principalTable: "Images",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SubscriptionLimits_Subscriptions_SubscriptionId",
column: x => x.SubscriptionId,
principalTable: "Subscriptions",
principalColumn: "Id");
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Users_SubscriptionId",
table: "Users",
column: "SubscriptionId");
migrationBuilder.CreateIndex(
name: "IX_SubscriptionLimits_ImageId",
table: "SubscriptionLimits",
column: "ImageId");
migrationBuilder.CreateIndex(
name: "IX_SubscriptionLimits_SubscriptionId",
table: "SubscriptionLimits",
column: "SubscriptionId");
migrationBuilder.AddForeignKey(
name: "FK_Users_Subscriptions_SubscriptionId",
table: "Users",
column: "SubscriptionId",
principalTable: "Subscriptions",
principalColumn: "Id");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedNewSubscriptionData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "CurrentSubscriptionId",
table: "Users",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SubscriptionDuration",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "SubscriptionSince",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateTable(
name: "Subscriptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LimitsJson = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Subscriptions", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Users_CurrentSubscriptionId",
table: "Users",
column: "CurrentSubscriptionId");
migrationBuilder.AddForeignKey(
name: "FK_Users_Subscriptions_CurrentSubscriptionId",
table: "Users",
column: "CurrentSubscriptionId",
principalTable: "Subscriptions",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Users_Subscriptions_CurrentSubscriptionId",
table: "Users");
migrationBuilder.DropTable(
name: "Subscriptions");
migrationBuilder.DropIndex(
name: "IX_Users_CurrentSubscriptionId",
table: "Users");
migrationBuilder.DropColumn(
name: "CurrentSubscriptionId",
table: "Users");
migrationBuilder.DropColumn(
name: "SubscriptionDuration",
table: "Users");
migrationBuilder.DropColumn(
name: "SubscriptionSince",
table: "Users");
}
}
}

View file

@ -482,6 +482,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("Installing")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsCleanupException")
.HasColumnType("tinyint(1)");
b.Property<int>("MainAllocationId")
.HasColumnType("int");
@ -606,14 +609,11 @@ namespace Moonlight.App.Database.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Duration")
.HasColumnType("int");
b.Property<string>("Name")
b.Property<string>("LimitsJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("SellPassId")
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
@ -622,39 +622,6 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Subscriptions");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SubscriptionLimit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<int>("Cpu")
.HasColumnType("int");
b.Property<int>("Disk")
.HasColumnType("int");
b.Property<int>("ImageId")
.HasColumnType("int");
b.Property<int>("Memory")
.HasColumnType("int");
b.Property<int?>("SubscriptionId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ImageId");
b.HasIndex("SubscriptionId");
b.ToTable("SubscriptionLimits");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
{
b.Property<int>("Id")
@ -723,6 +690,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("CurrentSubscriptionId")
.HasColumnType("int");
b.Property<long>("DiscordId")
.HasColumnType("bigint");
@ -752,10 +722,7 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("SubscriptionDuration")
.HasColumnType("int");
b.Property<int?>("SubscriptionId")
.HasColumnType("int");
b.Property<DateTime?>("SubscriptionSince")
b.Property<DateTime>("SubscriptionSince")
.HasColumnType("datetime(6)");
b.Property<bool>("SupportPending")
@ -776,7 +743,7 @@ namespace Moonlight.App.Database.Migrations
b.HasKey("Id");
b.HasIndex("SubscriptionId");
b.HasIndex("CurrentSubscriptionId");
b.ToTable("Users");
});
@ -966,21 +933,6 @@ namespace Moonlight.App.Database.Migrations
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SubscriptionLimit", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Image", "Image")
.WithMany()
.HasForeignKey("ImageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Subscription", null)
.WithMany("Limits")
.HasForeignKey("SubscriptionId");
b.Navigation("Image");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
@ -998,11 +950,11 @@ namespace Moonlight.App.Database.Migrations
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Subscription", "Subscription")
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
.WithMany()
.HasForeignKey("SubscriptionId");
.HasForeignKey("CurrentSubscriptionId");
b.Navigation("Subscription");
b.Navigation("CurrentSubscription");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
@ -1044,11 +996,6 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("Variables");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b =>
{
b.Navigation("Limits");
});
#pragma warning restore 612, 618
}
}

View file

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components.Forms;
namespace Moonlight.App.Helpers;
public class FieldCssHelper : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
return editContext.GetValidationMessages(fieldIdentifier).Any() ? "is-invalid" : "is-valid";
}
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Files;
public class ContextAction
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public Action<FileData> Action { get; set; }
}

View file

@ -0,0 +1,24 @@
namespace Moonlight.App.Helpers.Files;
public abstract class FileAccess : ICloneable
{
public string CurrentPath { get; set; } = "/";
public abstract Task<FileData[]> Ls();
public abstract Task Cd(string dir);
public abstract Task Up();
public abstract Task SetDir(string dir);
public abstract Task<string> Read(FileData fileData);
public abstract Task Write(FileData fileData, string content);
public abstract Task Upload(string name, Stream stream, Action<int>? progressUpdated = null);
public abstract Task MkDir(string name);
public abstract Task<string> Pwd();
public abstract Task<string> DownloadUrl(FileData fileData);
public abstract Task<Stream> DownloadStream(FileData fileData);
public abstract Task Delete(FileData fileData);
public abstract Task Move(FileData fileData, string newPath);
public abstract Task Compress(params FileData[] files);
public abstract Task Decompress(FileData fileData);
public abstract Task<string> GetLaunchUrl();
public abstract object Clone();
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Files;
public class FileData
{
public string Name { get; set; } = "";
public long Size { get; set; }
public bool IsFile { get; set; }
}

View file

@ -0,0 +1,232 @@
using System.Web;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Wings.Requests;
using Moonlight.App.Models.Wings.Resources;
using Moonlight.App.Services;
using RestSharp;
namespace Moonlight.App.Helpers.Files;
public class WingsFileAccess : FileAccess
{
private readonly WingsApiHelper WingsApiHelper;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly ConfigService ConfigService;
private readonly Server Server;
private readonly User User;
public WingsFileAccess(
WingsApiHelper wingsApiHelper,
WingsJwtHelper wingsJwtHelper,
Server server,
ConfigService configService,
User user)
{
WingsApiHelper = wingsApiHelper;
WingsJwtHelper = wingsJwtHelper;
Server = server;
ConfigService = configService;
User = user;
if (server.Node == null)
{
throw new ArgumentException("The wings file access server model needs to include the node data");
}
}
public override async Task<FileData[]> Ls()
{
var res = await WingsApiHelper.Get<ListDirectory[]>(
Server.Node,
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
);
var x = new List<FileData>();
foreach (var response in res)
{
x.Add(new()
{
Name = response.Name,
Size = response.File ? response.Size : 0,
IsFile = response.File,
});
}
return x.ToArray();
}
public override Task Cd(string dir)
{
var x = Path.Combine(CurrentPath, dir).Replace("\\", "/") + "/";
x = x.Replace("//", "/");
CurrentPath = x;
return Task.CompletedTask;
}
public override Task Up()
{
CurrentPath = Path.GetFullPath(Path.Combine(CurrentPath, "..")).Replace("\\", "/").Replace("C:", "");
return Task.CompletedTask;
}
public override Task SetDir(string dir)
{
CurrentPath = dir;
return Task.CompletedTask;
}
public override async Task<string> Read(FileData fileData)
{
return await WingsApiHelper.GetRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/contents?file={CurrentPath}{fileData.Name}");
}
public override async Task Write(FileData fileData, string content)
{
await WingsApiHelper.PostRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/write?file={CurrentPath}{fileData.Name}", content);
}
public override async Task Upload(string name, Stream dataStream, Action<int>? progressUpdated = null)
{
var token = WingsJwtHelper.Generate(
Server.Node.Token,
claims => { claims.Add("server_uuid", Server.Uuid.ToString()); }
);
var client = new RestClient();
var request = new RestRequest();
if (Server.Node.Ssl)
request.Resource =
$"https://{Server.Node.Fqdn}:{Server.Node.HttpPort}/upload/file?token={token}&directory={CurrentPath}";
else
request.Resource =
$"http://{Server.Node.Fqdn}:{Server.Node.HttpPort}/upload/file?token={token}&directory={CurrentPath}";
request.AddParameter("name", "files");
request.AddParameter("filename", name);
request.AddHeader("Content-Type", "multipart/form-data");
request.AddHeader("Origin", ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl"));
request.AddFile("files", () =>
{
return new StreamProgressHelper(dataStream)
{
Progress = i => { progressUpdated?.Invoke(i); }
};
}, name);
await client.ExecutePostAsync(request);
client.Dispose();
dataStream.Close();
}
public override async Task MkDir(string name)
{
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/create-directory",
new CreateDirectory()
{
Name = name,
Path = CurrentPath
}
);
}
public override Task<string> Pwd()
{
return Task.FromResult(CurrentPath);
}
public override Task<string> DownloadUrl(FileData fileData)
{
var token = WingsJwtHelper.Generate(Server.Node.Token, claims =>
{
claims.Add("server_uuid", Server.Uuid.ToString());
claims.Add("file_path", CurrentPath + "/" + fileData.Name);
});
if (Server.Node.Ssl)
{
return Task.FromResult(
$"https://{Server.Node.Fqdn}:{Server.Node.HttpPort}/download/file?token={token}"
);
}
else
{
return Task.FromResult(
$"http://{Server.Node.Fqdn}:{Server.Node.HttpPort}/download/file?token={token}"
);
}
}
public override Task<Stream> DownloadStream(FileData fileData)
{
throw new NotImplementedException();
}
public override async Task Delete(FileData fileData)
{
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFiles()
{
Root = CurrentPath,
Files = new()
{
fileData.Name
}
});
}
public override async Task Move(FileData fileData, string newPath)
{
var req = new RenameFiles()
{
Root = "/",
Files = new[]
{
new RenameFilesData()
{
From = (CurrentPath + fileData.Name),
To = newPath
}
}
};
await WingsApiHelper.Put(Server.Node, $"api/servers/{Server.Uuid}/files/rename", req);
}
public override async Task Compress(params FileData[] files)
{
var req = new CompressFiles()
{
Root = CurrentPath,
Files = files.Select(x => x.Name).ToArray()
};
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/compress", req);
}
public override async Task Decompress(FileData fileData)
{
var req = new DecompressFile()
{
Root = CurrentPath,
File = fileData.Name
};
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req);
}
public override Task<string> GetLaunchUrl()
{
return Task.FromResult(
$"sftp://{User.Id}.{StringHelper.IntToStringWithLeadingZeros(Server.Id, 8)}@{Server.Node.Fqdn}:{Server.Node.SftpPort}");
}
public override object Clone()
{
return new WingsFileAccess(WingsApiHelper, WingsJwtHelper, Server, ConfigService, User);
}
}

View file

@ -24,11 +24,11 @@ public class PaperApiHelper
else
requrl = ApiUrl + "/" + url;
RestRequest request = new(requrl);
RestRequest request = new(requrl, Method.Get);
request.AddHeader("Content-Type", "application/json");
var response = await client.GetAsync(request);
var response = await client.ExecuteAsync(request);
if (!response.IsSuccessful)
{

View file

@ -14,24 +14,13 @@ public class WingsApiHelper
Client = new();
}
private string GetApiUrl(Node node)
{
if(node.Ssl)
return $"https://{node.Fqdn}:{node.HttpPort}/";
else
return $"http://{node.Fqdn}:{node.HttpPort}/";
//return $"https://{node.Fqdn}:{node.HttpPort}/";
}
public async Task<T> Get<T>(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Get;
var response = await Client.GetAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -53,13 +42,11 @@ public class WingsApiHelper
public async Task<string> GetRaw(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Get;
var response = await Client.GetAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -81,18 +68,16 @@ public class WingsApiHelper
public async Task<T> Post<T>(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
request.AddParameter("text/plain",
JsonConvert.SerializeObject(body),
ParameterType.RequestBody
);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -114,16 +99,14 @@ public class WingsApiHelper
public async Task Post(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
if(body != null)
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -143,15 +126,13 @@ public class WingsApiHelper
public async Task PostRaw(Node node, string resource, object body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
request.AddParameter("text/plain", body, ParameterType.RequestBody);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -171,16 +152,14 @@ public class WingsApiHelper
public async Task Delete(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Delete;
if(body != null)
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.DeleteAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -200,15 +179,13 @@ public class WingsApiHelper
public async Task Put(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Put;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.PutAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -225,4 +202,20 @@ public class WingsApiHelper
}
}
}
private RestRequest CreateRequest(Node node, string resource)
{
var url = (node.Ssl ? "https" : "http") + $"://{node.Fqdn}:{node.HttpPort}/" + resource;
var request = new RestRequest(url)
{
Timeout = 60 * 15
};
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
return request;
}
}

View file

@ -80,6 +80,13 @@ public class WingsServerConverter
wingsServer.Settings.Environment.Add(v.Key, v.Value);
}
}
int i = 0;
foreach (var allocation in server.Allocations)
{
wingsServer.Settings.Environment.Add("ML_PORT_" + i, allocation.Port.ToString());
i++;
}
// Stop
if (image.StopCommand.StartsWith("!"))

View file

@ -1,50 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Services;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
[ApiController]
[Route("api/moonlight/payments")]
public class PaymentsController : Controller
{
private readonly ConfigService ConfigService;
private readonly SubscriptionService SubscriptionService;
public PaymentsController(ConfigService configService, SubscriptionService subscriptionService)
{
ConfigService = configService;
SubscriptionService = subscriptionService;
}
[HttpGet("generate")]
public async Task<ActionResult> GenerateGet([FromQuery] string key, [FromQuery] int subscriptionId)
{
var validKey = ConfigService
.GetSection("Moonlight")
.GetSection("Payments")
.GetValue<string>("Key");
if (key != validKey)
return StatusCode(403);
var token = await SubscriptionService.ProcessGenerate(subscriptionId);
return Ok(token);
}
[HttpPost("generate")]
public async Task<ActionResult> GeneratePost([FromQuery] string key, [FromQuery] int subscriptionId)
{
var validKey = ConfigService
.GetSection("Moonlight")
.GetSection("Payments")
.GetValue<string>("Key");
if (key != validKey)
return StatusCode(403);
var token = await SubscriptionService.ProcessGenerate(subscriptionId);
return Ok(token);
}
}

View file

@ -21,7 +21,10 @@ public class ResourcesController : Controller
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, name);
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
return NotFound();
}

View file

@ -39,7 +39,7 @@ public class WingsFileAccess : IFileAccess
public async Task<FileManagerObject[]> GetDirectoryContent()
{
var res = await WingsApiHelper.Get<ListDirectoryRequest[]>(Node,
var res = await WingsApiHelper.Get<ListDirectory[]>(Node,
$"api/servers/{Server.Uuid}/files/list-directory?directory={Path}");
var x = new List<FileManagerObject>();
@ -130,7 +130,7 @@ public class WingsFileAccess : IFileAccess
public async Task CreateDirectory(string name)
{
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/create-directory",
new CreateDirectoryRequest()
new CreateDirectory()
{
Name = name,
Path = Path
@ -171,7 +171,7 @@ public class WingsFileAccess : IFileAccess
public async Task Delete(FileManagerObject managerObject)
{
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFilesRequest()
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFiles()
{
Root = Path,
Files = new()
@ -183,7 +183,7 @@ public class WingsFileAccess : IFileAccess
public async Task Move(FileManagerObject managerObject, string newPath)
{
await WingsApiHelper.Put(Node, $"api/servers/{Server.Uuid}/files/rename", new RenameFilesRequest()
await WingsApiHelper.Put(Node, $"api/servers/{Server.Uuid}/files/rename", new RenameFiles()
{
Root = "/",
Files = new[]

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Files;
public class FileContextAction
{
public string Id { get; set; }
public string Name { get; set; }
public Action<FileManagerObject> Action { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class LoginDataModel
{
[Required(ErrorMessage = "You need to enter an email address")]
[EmailAddress(ErrorMessage = "You need to enter a valid email address")]
public string Email { get; set; }
[Required(ErrorMessage = "You need to enter a password")]
[MinLength(8, ErrorMessage = "You need to enter a password with minimum 8 characters in lenght")]
public string Password { get; set; }
}

View file

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class LoginTotpDataModel
{
[Required(ErrorMessage = "You need to enter a 2fa code")]
public string Code { get; set; } = "";
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class NameModel
{
[Required]
[MinLength(2, ErrorMessage = "Do you think, that works?")]
public string FirstName { get; set; }
[Required]
[MinLength(2, ErrorMessage = "Do you think, that works?")]
public string LastName { get; set; }
}

View file

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class PasswordModel
{
[Required(ErrorMessage = "You need to enter a password")]
[MinLength(8, ErrorMessage = "You need to enter a password with minimum 8 characters in lenght")]
public string Password { get; set; }
}

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class SubscriptionDataModel
{
[Required(ErrorMessage = "You need to enter a name")]
[MaxLength(32, ErrorMessage = "Max lenght for name is 32")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "You need to enter a description")]
public string Description { get; set; } = "";
}

View file

@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Log;
public class LogData
{
public Type Type { get; set; }
public string Value { get; set; }
}

View file

@ -19,5 +19,9 @@ public enum AuditLogType
AddDomainRecord,
UpdateDomainRecord,
DeleteDomainRecord,
PasswordReset
PasswordReset,
CleanupEnabled,
CleanupDisabled,
CleanupTriggered,
PasswordChange,
}

View file

@ -0,0 +1,14 @@
namespace Moonlight.App.Models.Misc;
public class SubscriptionLimit
{
public string Identifier { get; set; } = "";
public int Amount { get; set; }
public List<LimitOption> Options { get; set; } = new();
public class LimitOption
{
public string Key { get; set; } = "";
public string Value { get; set; } = "";
}
}

View file

@ -9,5 +9,6 @@ public enum UserStatus
Warned,
Banned,
Disabled,
DataPending
DataPending,
PasswordPending
}

View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Wings.Requests;
public class CompressFiles
{
[JsonProperty("root")]
public string Root { get; set; }
[JsonProperty("files")]
public string[] Files { get; set; }
}

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateBackupRequest
public class CreateBackup
{
[JsonProperty("adapter")]
public string Adapter { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateDirectoryRequest
public class CreateDirectory
{
[JsonProperty("name")]
public string Name { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateServerRequest
public class CreateServer
{
[JsonProperty("uuid")]
public Guid Uuid { get; set; }

View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Wings.Requests;
public class DecompressFile
{
[JsonProperty("root")]
public string Root { get; set; }
[JsonProperty("file")]
public string File { get; set; }
}

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class DeleteFilesRequest
public class DeleteFiles
{
[JsonProperty("root")]
public string Root { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class RenameFilesRequest
public class RenameFiles
{
[JsonProperty("root")]
public string Root { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class RestoreBackupRequest
public class RestoreBackup
{
[JsonProperty("adapter")]
public string Adapter { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class ServerPowerRequest
public class ServerPower
{
[JsonProperty("action")]
public string Action { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Resources;
public class ListDirectoryRequest
public class ListDirectory
{
[JsonProperty("name")]
public string Name { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Resources;
public class ServerDetailsResponse
public class ServerDetails
{
[JsonProperty("state")]
public string State { get; set; }
@ -11,9 +11,9 @@ public class ServerDetailsResponse
public bool IsSuspended { get; set; }
[JsonProperty("utilization")]
public ServerDetailsResponseUtilization Utilization { get; set; }
public ServerDetailsUtilization Utilization { get; set; }
public class ServerDetailsResponseUtilization
public class ServerDetailsUtilization
{
[JsonProperty("memory_bytes")]
public long MemoryBytes { get; set; }
@ -25,7 +25,7 @@ public class ServerDetailsResponse
public double CpuAbsolute { get; set; }
[JsonProperty("network")]
public ServerDetailsResponseNetwork Network { get; set; }
public ServerDetailsNetwork Network { get; set; }
[JsonProperty("uptime")]
public long Uptime { get; set; }
@ -37,7 +37,7 @@ public class ServerDetailsResponse
public long DiskBytes { get; set; }
}
public class ServerDetailsResponseNetwork
public class ServerDetailsNetwork
{
[JsonProperty("rx_bytes")]
public long RxBytes { get; set; }

View file

@ -2,7 +2,7 @@
using Moonlight.App.Database;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Repositories.Subscriptions;
namespace Moonlight.App.Repositories;
public class SubscriptionRepository : IDisposable
{

View file

@ -1,44 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Repositories.Subscriptions;
public class SubscriptionLimitRepository : IDisposable
{
private readonly DataContext DataContext;
public SubscriptionLimitRepository(DataContext dataContext)
{
DataContext = dataContext;
}
public DbSet<SubscriptionLimit> Get()
{
return DataContext.SubscriptionLimits;
}
public SubscriptionLimit Add(SubscriptionLimit subscription)
{
var x = DataContext.SubscriptionLimits.Add(subscription);
DataContext.SaveChanges();
return x.Entity;
}
public void Update(SubscriptionLimit subscription)
{
DataContext.SubscriptionLimits.Update(subscription);
DataContext.SaveChanges();
}
public void Delete(SubscriptionLimit subscription)
{
DataContext.SubscriptionLimits.Remove(subscription);
DataContext.SaveChanges();
}
public void Dispose()
{
DataContext.Dispose();
}
}

View file

@ -0,0 +1,216 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using MineStatLib;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Daemon.Resources;
using Moonlight.App.Models.Wings;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
using Logging.Net;
using Newtonsoft.Json;
namespace Moonlight.App.Services;
public class CleanupService
{
#region Stats
public DateTime StartedAt { get; private set; }
public DateTime CompletedAt { get; private set; }
public int ServersCleaned { get; private set; }
public int CleanupsPerformed { get; private set; }
public int ServersRunning { get; private set; }
public bool IsRunning { get; private set; }
#endregion
private readonly ConfigService ConfigService;
private readonly MessageService MessageService;
private readonly IServiceScopeFactory ServiceScopeFactory;
private readonly PeriodicTimer Timer;
public CleanupService(
ConfigService configService,
IServiceScopeFactory serviceScopeFactory,
MessageService messageService)
{
ServiceScopeFactory = serviceScopeFactory;
MessageService = messageService;
ConfigService = configService;
StartedAt = DateTime.Now;
CompletedAt = DateTime.Now;
IsRunning = false;
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup");
if (!config.GetValue<bool>("Enable") || ConfigService.DebugMode)
{
Logger.Info("Disabling cleanup service");
return;
}
Timer = new(TimeSpan.FromMinutes(config.GetValue<int>("Wait")));
Task.Run(Run);
}
private async Task Run()
{
while (await Timer.WaitForNextTickAsync())
{
IsRunning = true;
using var scope = ServiceScopeFactory.CreateScope();
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup");
var maxCpu = config.GetValue<int>("Cpu");
var minMemory = config.GetValue<int>("Memory");
var maxUptime = config.GetValue<int>("Uptime");
var minUptime = config.GetValue<int>("MinUptime");
var nodeRepository = scope.ServiceProvider.GetRequiredService<NodeRepository>();
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
var nodes = nodeRepository
.Get()
.ToArray();
foreach (var node in nodes)
{
try
{
var cpuStats = await nodeService.GetCpuStats(node);
var memoryStats = await nodeService.GetMemoryStats(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory)
{
var containerStats = await nodeService.GetContainerStats(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
var images = imageRepository
.Get()
.ToArray();
var imagesWithFlag = images
.Where(x =>
(JsonConvert.DeserializeObject<string[]>(x.TagsJson) ?? Array.Empty<string>()).Contains("cleanup")
)
.ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>();
foreach (var container in containerStats.Containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{
var server = serverRepository
.Get()
.Include(x => x.Image)
.Include(x => x.MainAllocation)
.Include(x => x.Variables)
.FirstOrDefault(x => x.Uuid == uuid);
if (server != null && imagesWithFlag.Any(y => y.Id == server.Image.Id))
{
containerMappedToServers.Add(container, server);
}
}
}
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
foreach (var containerMapped in containerMappedToServers)
{
var server = containerMapped.Value;
try
{
var stats = await serverService.GetDetails(server);
if (server.IsCleanupException)
{
if (stats.Utilization.Uptime > TimeSpan.FromHours(maxUptime).TotalMilliseconds)
{
var players = GetPlayers(node, server.MainAllocation);
if (players == 0)
{
await serverService.SetPowerState(server, PowerSignal.Restart);
ServersCleaned++;
}
else
{
ServersRunning++;
}
await MessageService.Emit("cleanup.updated", null);
}
}
else
{
if (stats.Utilization.Uptime > TimeSpan.FromMinutes(minUptime).TotalMilliseconds)
{
var players = GetPlayers(node, server.MainAllocation);
if (players < 1)
{
var j2SVar = server.Variables.FirstOrDefault(x => x.Key == "J2S");
var handleJ2S = j2SVar != null && j2SVar.Value == "1";
if (handleJ2S)
{
await serverService.SetPowerState(server, PowerSignal.Restart);
}
else
{
await serverService.SetPowerState(server, PowerSignal.Stop);
}
ServersCleaned++;
}
else
{
ServersRunning++;
}
await MessageService.Emit("cleanup.updated", null);
}
}
}
catch (Exception e)
{
Logger.Warn($"Error checking server {server.Name} ({server.Id})");
Logger.Warn(e);
}
}
}
}
catch (Exception e)
{
Logger.Error($"Error performing cleanup on node {node.Name} ({node.Id})");
Logger.Error(e);
}
}
IsRunning = false;
CleanupsPerformed++;
await MessageService.Emit("cleanup.updated", null);
}
}
private int GetPlayers(Node node, NodeAllocation allocation)
{
var ms = new MineStat(node.Fqdn, (ushort)allocation.Port);
//TODO: Add fake player check
if (ms.ServerUp)
{
return ms.CurrentPlayersInt;
}
return -1;
}
}

View file

@ -1,4 +1,5 @@
using System.Text;
using Logging.Net;
using Microsoft.Extensions.Primitives;
using Moonlight.App.Helpers;
@ -21,6 +22,9 @@ public class ConfigService : IConfiguration
if (debugVar != null)
DebugMode = bool.Parse(debugVar);
if(DebugMode)
Logger.Debug("Debug mode enabled");
}
public IEnumerable<IConfigurationSection> GetChildren()

View file

@ -169,7 +169,11 @@ public class DomainService
}));
}
await AuditLogService.Log(AuditLogType.AddDomainRecord, new[] { d.Id.ToString(), dnsRecord.Name });
await AuditLogService.Log(AuditLogType.AddDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
}
public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord)
@ -199,7 +203,11 @@ public class DomainService
}));
}
await AuditLogService.Log(AuditLogType.UpdateDomainRecord, new[] { d.Id.ToString(), dnsRecord.Name });
await AuditLogService.Log(AuditLogType.UpdateDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
}
public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord)
@ -210,7 +218,11 @@ public class DomainService
await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id)
);
await AuditLogService.Log(AuditLogType.DeleteDomainRecord, new[] { d.Id.ToString(), dnsRecord.Name });
await AuditLogService.Log(AuditLogType.DeleteDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
}
private Domain EnsureData(Domain domain)

View file

@ -0,0 +1,23 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class ModalService
{
private readonly IJSRuntime JsRuntime;
public ModalService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task Show(string name)
{
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", name);
}
public async Task Hide(string name)
{
await JsRuntime.InvokeVoidAsync("moonlight.modals.hide", name);
}
}

View file

@ -30,4 +30,19 @@ public class ToastService
{
await JsRuntime.InvokeVoidAsync("showSuccessToast", message);
}
public async Task CreateProcessToast(string id, string text)
{
await JsRuntime.InvokeVoidAsync("createToast", id, text);
}
public async Task UpdateProcessToast(string id, string text)
{
await JsRuntime.InvokeVoidAsync("modifyToast", id, text);
}
public async Task RemoveProcessToast(string id)
{
await JsRuntime.InvokeVoidAsync("removeToast", id);
}
}

View file

@ -1,4 +1,5 @@
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
@ -19,16 +20,18 @@ public class AuditLogService
HttpContextAccessor = httpContextAccessor;
}
public Task Log(AuditLogType type, params object[] data)
public Task Log(AuditLogType type, Action<AuditLogParameters> data)
{
var ip = GetIp();
var al = new AuditLogParameters();
data(al);
var entry = new AuditLogEntry()
{
Ip = ip,
Type = type,
System = false,
JsonData = data.Length == 0 ? "" : JsonConvert.SerializeObject(data)
JsonData = al.Build()
};
Repository.Add(entry);
@ -36,13 +39,16 @@ public class AuditLogService
return Task.CompletedTask;
}
public Task LogSystem(AuditLogType type, params object[] data)
public Task LogSystem(AuditLogType type, Action<AuditLogParameters> data)
{
var al = new AuditLogParameters();
data(al);
var entry = new AuditLogEntry()
{
Type = type,
System = true,
JsonData = data.Length == 0 ? "" : JsonConvert.SerializeObject(data)
JsonData = al.Build()
};
Repository.Add(entry);
@ -62,4 +68,23 @@ public class AuditLogService
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class AuditLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object data)
{
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View file

@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Reflection;
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
@ -18,15 +19,17 @@ public class ErrorLogService
HttpContextAccessor = httpContextAccessor;
}
public Task Log(Exception exception, params object[] objects)
public Task Log(Exception exception, Action<ErrorLogParameters> data)
{
var ip = GetIp();
var al = new ErrorLogParameters();
data(al);
var entry = new ErrorLogEntry()
{
Ip = ip,
System = false,
JsonData = !objects.Any() ? "" : JsonConvert.SerializeObject(objects),
JsonData = al.Build(),
Class = NameOfCallingClass(),
Stacktrace = exception.ToStringDemystified()
};
@ -36,12 +39,15 @@ public class ErrorLogService
return Task.CompletedTask;
}
public Task LogSystem(Exception exception, params object[] objects)
public Task LogSystem(Exception exception, Action<ErrorLogParameters> data)
{
var al = new ErrorLogParameters();
data(al);
var entry = new ErrorLogEntry()
{
System = true,
JsonData = !objects.Any() ? "" : JsonConvert.SerializeObject(objects),
JsonData = al.Build(),
Class = NameOfCallingClass(),
Stacktrace = exception.ToStringDemystified()
};
@ -87,4 +93,23 @@ public class ErrorLogService
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class ErrorLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object data)
{
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View file

@ -1,4 +1,5 @@
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
@ -17,16 +18,18 @@ public class SecurityLogService
HttpContextAccessor = httpContextAccessor;
}
public Task Log(SecurityLogType type, params object[] data)
public Task Log(SecurityLogType type, Action<SecurityLogParameters> data)
{
var ip = GetIp();
var al = new SecurityLogParameters();
data(al);
var entry = new SecurityLogEntry()
{
Ip = ip,
Type = type,
System = false,
JsonData = data.Length == 0 ? "" : JsonConvert.SerializeObject(data)
JsonData = al.Build()
};
Repository.Add(entry);
@ -34,13 +37,16 @@ public class SecurityLogService
return Task.CompletedTask;
}
public Task LogSystem(SecurityLogType type, params object[] data)
public Task LogSystem(SecurityLogType type, Action<SecurityLogParameters> data)
{
var al = new SecurityLogParameters();
data(al);
var entry = new SecurityLogEntry()
{
Type = type,
System = true,
JsonData = data.Length == 0 ? "" : JsonConvert.SerializeObject(data)
JsonData = al.Build()
};
Repository.Add(entry);
@ -60,4 +66,24 @@ public class SecurityLogService
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class SecurityLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object data)
{
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View file

@ -76,7 +76,10 @@ public class OneTimeJwtService
}
catch (SignatureVerificationException)
{
await SecurityLogService.LogSystem(SecurityLogType.ManipulatedJwt, token);
await SecurityLogService.LogSystem(SecurityLogType.ManipulatedJwt, x =>
{
x.Add<string>(token);
});
return null;
}
catch (Exception e)

View file

@ -75,11 +75,11 @@ public class ServerService
return s;
}
public async Task<ServerDetailsResponse> GetDetails(Server s)
public async Task<ServerDetails> GetDetails(Server s)
{
Server server = EnsureNodeData(s);
return await WingsApiHelper.Get<ServerDetailsResponse>(
return await WingsApiHelper.Get<ServerDetails>(
server.Node,
$"api/servers/{server.Uuid}"
);
@ -91,12 +91,16 @@ public class ServerService
var rawSignal = signal.ToString().ToLower();
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPowerRequest()
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower()
{
Action = rawSignal
});
await AuditLogService.Log(AuditLogType.ChangePowerState, new[] { server.Uuid.ToString(), rawSignal });
await AuditLogService.Log(AuditLogType.ChangePowerState, x =>
{
x.Add<Server>(server.Uuid);
x.Add<PowerSignal>(rawSignal);
});
}
public async Task<ServerBackup> CreateBackup(Server server)
@ -118,7 +122,7 @@ public class ServerService
serverData.Backups.Add(backup);
ServerRepository.Update(serverData);
await WingsApiHelper.Post(serverData.Node, $"api/servers/{serverData.Uuid}/backup", new CreateBackupRequest()
await WingsApiHelper.Post(serverData.Node, $"api/servers/{serverData.Uuid}/backup", new CreateBackup()
{
Adapter = "wings",
Uuid = backup.Uuid,
@ -126,7 +130,11 @@ public class ServerService
});
await AuditLogService.Log(AuditLogType.CreateBackup,
new[] { serverData.Uuid.ToString(), backup.Uuid.ToString() });
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
});
return backup;
}
@ -158,13 +166,17 @@ public class ServerService
Server server = EnsureNodeData(s);
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/backup/{serverBackup.Uuid}/restore",
new RestoreBackupRequest()
new RestoreBackup()
{
Adapter = "wings"
});
await AuditLogService.Log(AuditLogType.RestoreBackup,
new[] { s.Uuid.ToString(), serverBackup.Uuid.ToString() });
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
}
public async Task DeleteBackup(Server server, ServerBackup serverBackup)
@ -186,7 +198,11 @@ public class ServerService
await MessageService.Emit("wings.backups.delete", backup);
await AuditLogService.Log(AuditLogType.DeleteBackup,
new[] { serverBackup.Uuid.ToString(), serverBackup.Uuid.ToString() });
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
});
}
public async Task<string> DownloadBackup(Server s, ServerBackup serverBackup)
@ -200,7 +216,11 @@ public class ServerService
});
await AuditLogService.Log(AuditLogType.DownloadBackup,
new[] { serverBackup.Uuid.ToString(), serverBackup.Uuid.ToString() });
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
return $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}";
}
@ -299,19 +319,26 @@ public class ServerService
try
{
await WingsApiHelper.Post(node, $"api/servers", new CreateServerRequest()
await WingsApiHelper.Post(node, $"api/servers", new CreateServer()
{
Uuid = newServerData.Uuid,
StartOnCompletion = false
});
await AuditLogService.Log(AuditLogType.CreateServer, newServerData.Uuid.ToString());
await AuditLogService.Log(AuditLogType.CreateServer, x =>
{
x.Add<Server>(newServerData.Uuid);
});
return newServerData;
}
catch (Exception e)
{
await ErrorLogService.Log(e, new[] { newServerData.Uuid.ToString(), node.Id.ToString() });
await ErrorLogService.Log(e, x =>
{
x.Add<Server>(newServerData.Uuid);
x.Add<Node>(node.Id);
});
ServerRepository.Delete(newServerData);
@ -325,7 +352,10 @@ public class ServerService
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null);
await AuditLogService.Log(AuditLogType.ReinstallServer, server.Uuid.ToString());
await AuditLogService.Log(AuditLogType.ReinstallServer, x =>
{
x.Add<Server>(server.Uuid);
});
}
public async Task<Server> SftpServerLogin(int serverId, int id, string password)
@ -334,7 +364,10 @@ public class ServerService
if (server == null)
{
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, serverId);
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, x =>
{
x.Add<int>(id);
});
throw new Exception("Server not found");
}

View file

@ -89,12 +89,15 @@ public class IdentityService
}
catch (SignatureVerificationException)
{
await SecurityLogService.Log(SecurityLogType.ManipulatedJwt, token);
await SecurityLogService.Log(SecurityLogType.ManipulatedJwt, x =>
{
x.Add<string>(token);
});
return null;
}
catch (Exception e)
{
await ErrorLogService.Log(e);
await ErrorLogService.Log(e, x => {});
return null;
}
@ -130,7 +133,7 @@ public class IdentityService
}
catch (Exception e)
{
await ErrorLogService.Log(e);
await ErrorLogService.Log(e, x => {});
return null;
}
}

View file

@ -0,0 +1,45 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Newtonsoft.Json;
namespace Moonlight.App.Services;
public class SubscriptionAdminService
{
private readonly SubscriptionRepository SubscriptionRepository;
private readonly OneTimeJwtService OneTimeJwtService;
public SubscriptionAdminService(OneTimeJwtService oneTimeJwtService, SubscriptionRepository subscriptionRepository)
{
OneTimeJwtService = oneTimeJwtService;
SubscriptionRepository = subscriptionRepository;
}
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
{
return Task.FromResult(
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
?? Array.Empty<SubscriptionLimit>()
);
}
public Task SaveLimits(Subscription subscription, SubscriptionLimit[] limits)
{
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
SubscriptionRepository.Update(subscription);
return Task.CompletedTask;
}
public Task<string> GenerateCode(Subscription subscription, int duration)
{
return Task.FromResult(
OneTimeJwtService.Generate(data =>
{
data.Add("subscription", subscription.Id.ToString());
data.Add("duration", duration.ToString());
}, TimeSpan.FromDays(10324))
);
}
}

View file

@ -3,126 +3,138 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Subscriptions;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
namespace Moonlight.App.Services;
public class SubscriptionService
{
private readonly SubscriptionRepository SubscriptionRepository;
private readonly UserRepository UserRepository;
private readonly IdentityService IdentityService;
private readonly ConfigService ConfigService;
private readonly OneTimeJwtService OneTimeJwtService;
private readonly AuditLogService AuditLogService;
private readonly IdentityService IdentityService;
private readonly UserRepository UserRepository;
private readonly ConfigService ConfigService;
public SubscriptionService(SubscriptionRepository subscriptionRepository,
UserRepository userRepository,
IdentityService identityService,
ConfigService configService,
public SubscriptionService(
SubscriptionRepository subscriptionRepository,
OneTimeJwtService oneTimeJwtService,
AuditLogService auditLogService)
IdentityService identityService,
UserRepository userRepository,
ConfigService configService)
{
SubscriptionRepository = subscriptionRepository;
UserRepository = userRepository;
IdentityService = identityService;
ConfigService = configService;
OneTimeJwtService = oneTimeJwtService;
AuditLogService = auditLogService;
IdentityService = identityService;
UserRepository = userRepository;
ConfigService = configService;
}
public async Task<Subscription?> Get()
public async Task<Subscription?> GetCurrent()
{
var user = await IdentityService.Get();
var advancedUser = UserRepository
.Get()
.Include(x => x.Subscription)
.First(x => x.Id == user!.Id);
var user = await GetCurrentUser();
if (advancedUser.Subscription == null)
if (user == null || user.CurrentSubscription == null)
return null;
return SubscriptionRepository
.Get()
.Include(x => x.Limits)
.Include("Limits.Image")
.First(x => x.Id == advancedUser.Subscription.Id);
}
public async Task Cancel()
{
var user = await IdentityService.Get();
user!.Subscription = null;
UserRepository.Update(user!);
var subscriptionEnd = user.SubscriptionSince.ToUniversalTime().AddDays(user.SubscriptionDuration);
await AuditLogService.Log(AuditLogType.CancelSubscription, new[] { user.Email });
}
public Task<Subscription[]> GetAvailable()
{
return Task.FromResult(
SubscriptionRepository
.Get()
.Include(x => x.Limits)
.ToArray()
);
}
public Task<string> GenerateBuyUrl(Subscription subscription)
{
var url = ConfigService
.GetSection("Moonlight")
.GetSection("Payments")
.GetValue<string>("BaseUrl");
if (subscriptionEnd > DateTime.UtcNow)
{
return user.CurrentSubscription;
}
return Task.FromResult<string>($"{url}/products/{subscription.SellPassId}");
}
public Task<string> ProcessGenerate(int subscriptionId)
{
var subscription = SubscriptionRepository
.Get()
.FirstOrDefault(x => x.Id == subscriptionId);
if (subscription == null)
throw new DisplayException("Unknown subscription id");
var token = OneTimeJwtService.Generate(
options =>
{
options.Add("id", subscription.Id.ToString());
}
);
return Task.FromResult(token);
return null;
}
public async Task ApplyCode(string code)
{
var user = (await IdentityService.Get())!;
var values = await OneTimeJwtService.Validate(code);
var data = await OneTimeJwtService.Validate(code);
if (values == null)
throw new DisplayException("Invalid subscription code");
if (data == null)
throw new DisplayException("Invalid or expired subscription code");
if (!values.ContainsKey("id"))
throw new DisplayException("Subscription code is missing the id");
var id = int.Parse(values["id"]);
var id = int.Parse(data["subscription"]);
var duration = int.Parse(data["duration"]);
var subscription = SubscriptionRepository
.Get()
.FirstOrDefault(x => x.Id == id);
if (subscription == null)
throw new DisplayException("The subscription the code is referring does not exist");
throw new DisplayException("The subscription the code is associated with does not exist");
user.Subscription = subscription;
user.SubscriptionDuration = subscription.Duration;
user.SubscriptionSince = DateTime.Now;
var user = await GetCurrentUser();
if (user == null)
throw new DisplayException("Unable to determine current user");
user.CurrentSubscription = subscription;
user.SubscriptionDuration = duration;
user.SubscriptionSince = DateTime.UtcNow;
UserRepository.Update(user);
await OneTimeJwtService.Revoke(code);
await AuditLogService.Log(AuditLogType.ApplySubscriptionCode, new[] { user.Email, subscription.Id.ToString() });
await OneTimeJwtService.Revoke(code);
}
public async Task<SubscriptionLimit> GetLimit(string identifier)
{
var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions");
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("defaultLimits");
var subscription = await GetCurrent();
if (subscription == null)
{
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundDefault != null)
return foundDefault;
return new()
{
Identifier = identifier,
Amount = 0
};
}
else
{
var subscriptionLimits =
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
?? Array.Empty<SubscriptionLimit>();
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundLimit != null)
return foundLimit;
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundDefault != null)
return foundDefault;
return new()
{
Identifier = identifier,
Amount = 0
};
}
}
private async Task<User?> GetCurrentUser()
{
var user = await IdentityService.Get();
if (user == null)
return null;
var userWithData = UserRepository
.Get()
.Include(x => x.CurrentSubscription)
.First(x => x.Id == user.Id);
return userWithData;
}
}

View file

@ -1,4 +1,5 @@
using Moonlight.App.Models.Misc;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
@ -46,13 +47,23 @@ public class TotpService
public async Task Enable()
{
var user = (await IdentityService.Get())!;
user.TotpEnabled = true;
user.TotpSecret = GenerateSecret();
UserRepository.Update(user);
await AuditLogService.Log(AuditLogType.EnableTotp, user.Email);
await AuditLogService.Log(AuditLogType.EnableTotp, x =>
{
x.Add<User>(user.Email);
});
}
public async Task EnforceTotpLogin()
{
var user = (await IdentityService.Get())!;
user.TotpEnabled = true;
UserRepository.Update(user);
}
public async Task Disable()
@ -63,7 +74,10 @@ public class TotpService
UserRepository.Update(user);
await AuditLogService.Log(AuditLogType.DisableTotp, user.Email);
await AuditLogService.Log(AuditLogType.DisableTotp,x =>
{
x.Add<User>(user.Email);
});
}
private string GenerateSecret()

View file

@ -77,7 +77,10 @@ public class UserService
});
await MailService.SendMail(user!, "register", values => {});
await AuditLogService.Log(AuditLogType.Register, user.Email);
await AuditLogService.Log(AuditLogType.Register, x =>
{
x.Add<User>(user.Email);
});
return await GenerateToken(user);
}
@ -91,7 +94,11 @@ public class UserService
if (user == null)
{
await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password });
await SecurityLogService.Log(SecurityLogType.LoginFail, x =>
{
x.Add<User>(email);
x.Add<string>(password);
});
throw new DisplayException("Email and password combination not found");
}
@ -100,7 +107,11 @@ public class UserService
return user.TotpEnabled;
}
await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password });
await SecurityLogService.Log(SecurityLogType.LoginFail, x =>
{
x.Add<User>(email);
x.Add<string>(password);
});
throw new DisplayException("Email and password combination not found");;
}
@ -125,18 +136,28 @@ public class UserService
if (totpCodeValid)
{
await AuditLogService.Log(AuditLogType.Login, email);
await AuditLogService.Log(AuditLogType.Login, x =>
{
x.Add<User>(email);
});
return await GenerateToken(user, true);
}
else
{
await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password });
await SecurityLogService.Log(SecurityLogType.LoginFail, x =>
{
x.Add<User>(email);
x.Add<string>(password);
});
throw new DisplayException("2FA code invalid");
}
}
else
{
await AuditLogService.Log(AuditLogType.Login, email);
await AuditLogService.Log(AuditLogType.Login, x =>
{
x.Add<User>(email);
});
return await GenerateToken(user!, true);
}
}
@ -149,7 +170,10 @@ public class UserService
if (isSystemAction)
{
await AuditLogService.LogSystem(AuditLogType.ChangePassword, user.Email);
await AuditLogService.LogSystem(AuditLogType.ChangePassword, x=>
{
x.Add<User>(user.Email);
});
}
else
{
@ -160,7 +184,10 @@ public class UserService
values.Add("Location", "In your walls");
});
await AuditLogService.Log(AuditLogType.ChangePassword, user.Email);
await AuditLogService.Log(AuditLogType.ChangePassword, x =>
{
x.Add<User>(user.Email);
});
}
}
@ -170,17 +197,27 @@ public class UserService
if (user == null)
{
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, id);
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, x =>
{
x.Add<int>(id);
});
throw new Exception("Invalid username");
}
if (BCrypt.Net.BCrypt.Verify(password, user.Password))
{
await AuditLogService.LogSystem(AuditLogType.Login, user.Email);
await AuditLogService.LogSystem(AuditLogType.Login, x =>
{
x.Add<User>(user.Email);
});
return user;
}
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, new[] { id.ToString(), password });
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, x =>
{
x.Add<int>(id);
x.Add<string>(password);
});
throw new Exception("Invalid userid or password");
}
@ -218,7 +255,7 @@ public class UserService
var newPassword = StringHelper.GenerateString(16);
await ChangePassword(user, newPassword, true);
await AuditLogService.Log(AuditLogType.PasswordReset);
await AuditLogService.Log(AuditLogType.PasswordReset, x => {});
await MailService.SendMail(user, "passwordReset", values =>
{

View file

@ -58,6 +58,8 @@
<_ContentIncludedByDefault Remove="wwwroot\css\site.css" />
<_ContentIncludedByDefault Remove="Shared\Components\Tables\Column.razor" />
<_ContentIncludedByDefault Remove="Shared\Components\Tables\Table.razor" />
<_ContentIncludedByDefault Remove="Shared\Views\Admin\Servers\Cleanup\Exceptions\Add.razor" />
<_ContentIncludedByDefault Remove="Shared\Views\Admin\Servers\Cleanup\Exceptions\Edit.razor" />
</ItemGroup>
<ItemGroup>
@ -66,7 +68,6 @@
<Folder Include="App\Models\Google\Resources" />
<Folder Include="App\Services\DiscordBot\Commands" />
<Folder Include="resources\lang" />
<Folder Include="wwwroot\assets\media" />
</ItemGroup>
</Project>

View file

@ -101,6 +101,7 @@
<script>require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="/_content/BlazorMonaco/jsInterop.js"></script>
<script src="/assets/js/monacoTheme.js"></script>
<script src="/assets/js/scripts.bundle.js"></script>
<script src="/assets/js/flashbang.js"></script>
@ -112,5 +113,6 @@
<script src="/assets/js/loggingUtils.js"></script>
<script src="/assets/js/snow.js"></script>
<script src="/assets/js/recaptcha.js"></script>
<script src="/assets/js/moonlight.js"></script>
</body>
</html>

View file

@ -9,7 +9,6 @@ using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Domains;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Repositories.Servers;
using Moonlight.App.Repositories.Subscriptions;
using Moonlight.App.Services;
using Moonlight.App.Services.DiscordBot;
using Moonlight.App.Services.Interop;
@ -58,13 +57,12 @@ namespace Moonlight
builder.Services.AddScoped<SupportMessageRepository>();
builder.Services.AddScoped<DomainRepository>();
builder.Services.AddScoped<SharedDomainRepository>();
builder.Services.AddScoped<SubscriptionRepository>();
builder.Services.AddScoped<SubscriptionLimitRepository>();
builder.Services.AddScoped<RevokeRepository>();
builder.Services.AddScoped<NotificationRepository>();
builder.Services.AddScoped<AaPanelRepository>();
builder.Services.AddScoped<WebsiteRepository>();
builder.Services.AddScoped<DdosAttackRepository>();
builder.Services.AddScoped<SubscriptionRepository>();
builder.Services.AddScoped<AuditLogEntryRepository>();
builder.Services.AddScoped<ErrorLogEntryRepository>();
@ -89,14 +87,19 @@ namespace Moonlight
builder.Services.AddSingleton<ResourceService>();
builder.Services.AddScoped<DomainService>();
builder.Services.AddScoped<OneTimeJwtService>();
builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddSingleton<NotificationServerService>();
builder.Services.AddScoped<NotificationAdminService>();
builder.Services.AddScoped<NotificationClientService>();
builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<GoogleOAuth2Service>();
builder.Services.AddScoped<DiscordOAuth2Service>();
builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>();
builder.Services.AddSingleton<CleanupService>();
// Loggers
builder.Services.AddScoped<SecurityLogService>();
builder.Services.AddScoped<AuditLogService>();
@ -153,6 +156,9 @@ namespace Moonlight
// Support service
var supportServerService = app.Services.GetRequiredService<SupportServerService>();
// cleanup service
_ = app.Services.GetRequiredService<CleanupService>();
// Discord bot service
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();

View file

@ -3,6 +3,7 @@
@using Moonlight.App.Repositories
@using Newtonsoft.Json
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Log
@inject UserRepository UserRepository
@ -18,7 +19,7 @@
<div class="fs-5 fw-semibold mb-2">
@if (User == null)
{
<TL>Password change for</TL> @(Data[0])
<TL>Password change for</TL> @(Data[0].Value)
}
else
{
@ -38,18 +39,18 @@
public AuditLogEntry Entry { get; set; }
private User? User;
private string[] Data;
private LogData[] Data;
protected override void OnInitialized()
{
Data = JsonConvert.DeserializeObject<string[]>(Entry.JsonData)!;
Data = JsonConvert.DeserializeObject<LogData[]>(Entry.JsonData)!;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0]);
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0].Value);
await InvokeAsync(StateHasChanged);
}

View file

@ -2,6 +2,7 @@
@using Moonlight.App.Helpers
@using Newtonsoft.Json
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Log
@using Moonlight.App.Repositories.Servers
@inject ServerRepository ServerRepository
@ -18,11 +19,11 @@
<div class="fs-5 fw-semibold mb-2">
@if (Server == null)
{
<TL>Change power state for</TL> @(Data[0]) <TL>to</TL> @(Data[1])
<TL>Change power state for</TL> @(Data[0].Value) <TL>to</TL> @(Data[1].Value)
}
else
{
<TL>Change power state for</TL> <a href="/admin/servers/edit/@(Server.Id)">@(Server.Name)</a> <TL>to</TL> @(Data[1])
<TL>Change power state for</TL> <a href="/admin/servers/edit/@(Server.Id)">@(Server.Name)</a> <TL>to</TL> @(Data[1].Value)
}
</div>
<div class="d-flex align-items-center mt-1 fs-6">
@ -38,18 +39,18 @@
public AuditLogEntry Entry { get; set; }
private Server? Server;
private string[] Data;
private LogData[] Data;
protected override void OnInitialized()
{
Data = JsonConvert.DeserializeObject<string[]>(Entry.JsonData)!;
Data = JsonConvert.DeserializeObject<LogData[]>(Entry.JsonData)!;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Server = ServerRepository.Get().FirstOrDefault(x => x.Uuid == Guid.Parse(Data[0]));
Server = ServerRepository.Get().FirstOrDefault(x => x.Uuid == Guid.Parse(Data[0].Value));
await InvokeAsync(StateHasChanged);
}

View file

@ -3,6 +3,7 @@
@using Moonlight.App.Repositories
@using Newtonsoft.Json
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Log
@inject UserRepository UserRepository
@ -18,7 +19,7 @@
<div class="fs-5 fw-semibold mb-2">
@if (User == null)
{
<TL>New login for</TL> @(Data[0])
<TL>New login for</TL> @(Data[0].Value)
}
else
{
@ -38,18 +39,18 @@
public AuditLogEntry Entry { get; set; }
private User? User;
private string[] Data;
private LogData[] Data;
protected override void OnInitialized()
{
Data = JsonConvert.DeserializeObject<string[]>(Entry.JsonData)!;
Data = JsonConvert.DeserializeObject<LogData[]>(Entry.JsonData)!;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0]);
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0].Value);
await InvokeAsync(StateHasChanged);
}

View file

@ -3,6 +3,7 @@
@using Moonlight.App.Repositories
@using Newtonsoft.Json
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Log
@inject UserRepository UserRepository
@ -18,7 +19,7 @@
<div class="fs-5 fw-semibold mb-2">
@if (User == null)
{
<TL>Register for</TL> @(Data[0])
<TL>Register for</TL> @(Data[0].Value)
}
else
{
@ -38,18 +39,18 @@
public AuditLogEntry Entry { get; set; }
private User? User;
private string[] Data;
private LogData[] Data;
protected override void OnInitialized()
{
Data = JsonConvert.DeserializeObject<string[]>(Entry.JsonData)!;
Data = JsonConvert.DeserializeObject<LogData[]>(Entry.JsonData)!;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0]);
User = UserRepository.Get().FirstOrDefault(x => x.Email == Data[0].Value);
await InvokeAsync(StateHasChanged);
}

View file

@ -9,8 +9,12 @@
@using Moonlight.App.Services
@using Moonlight.App.Exceptions
@using Logging.Net
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services.OAuth2
@using Moonlight.App.Services.Sessions
@using System.ComponentModel.DataAnnotations
@using Moonlight.App.Models.Forms
@inject AlertService AlertService
@inject UserService UserService
@ -24,10 +28,10 @@
<div class="card rounded-3 w-md-550px">
<div class="card-body">
<div class="d-flex flex-center flex-column-fluid pb-15 pb-lg-20">
<div class="form w-100 fv-plugins-bootstrap5 fv-plugins-framework" novalidate="novalidate">
@if (!TotpRequired)
{
<div class="text-center mb-11">
@if (!TotpRequired)
{
<SmartForm Model="LoginData" OnValidSubmit="DoLogin">
<div class="text-center mt-3 mb-11">
<h1 class="text-dark fw-bolder mb-3">
<TL>Sign In</TL>
</h1>
@ -61,12 +65,12 @@
</span>
</div>
<div class="fv-row mb-8 fv-plugins-icon-container">
<input @bind="Email" type="text" placeholder="@(SmartTranslateService.Translate("Email"))" class="form-control bg-transparent">
<div class="mt-3 mb-3">
<InputText @bind-Value="LoginData.Email" type="email" placeholder="@(SmartTranslateService.Translate("Email"))" class="form-control bg-transparent"/>
</div>
<div class="fv-row mb-3 fv-plugins-icon-container">
<input @bind="Password" type="password" placeholder="@(SmartTranslateService.Translate("Password"))" class="form-control bg-transparent">
<div class="mb-3">
<InputText @bind-Value="LoginData.Password" type="password" placeholder="@(SmartTranslateService.Translate("Password"))" class="form-control bg-transparent"/>
</div>
<div class="d-flex flex-stack flex-wrap gap-3 fs-base fw-semibold mb-8">
@ -78,35 +82,33 @@
</div>
<div class="d-grid mb-10">
<WButton Text="@(SmartTranslateService.Translate("Sign-in"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
CssClasses="btn-primary"
OnClick="DoLogin">
</WButton>
<button type="submit" class="btn btn-primary">
<TL>Sign-in</TL>
</button>
</div>
}
else
{
<div class="text-gray-500 text-center fw-semibold fs-6">
<TL>Not registered yet?</TL>
<a href="/register" class="link-primary">
<TL>Sign up</TL>
</a>
</div>
</SmartForm>
}
else
{
<SmartForm Model="TotpData" OnValidSubmit="DoLogin">
<div class="fv-row mb-8 fv-plugins-icon-container">
<input type="number" class="form-control bg-transparent">
<InputText @bind-Value="TotpData.Code" type="number" class="form-control bg-transparent"></InputText>
</div>
<div class="d-grid mb-10">
<WButton Text="@(SmartTranslateService.Translate("Sign-in"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
CssClasses="btn-primary"
OnClick="DoLogin">
</WButton>
<button type="submit" class="btn btn-primary">
<TL>Sign-in</TL>
</button>
</div>
}
<div class="text-gray-500 text-center fw-semibold fs-6">
<TL>Not registered yet?</TL>
<a href="/register" class="link-primary">
<TL>Sign up</TL>
</a>
</div>
</div>
</SmartForm>
}
</div>
</div>
</div>
@ -114,37 +116,54 @@
@code
{
private string Email = "";
private string Password = "";
private LoginDataModel LoginData = new();
private LoginTotpDataModel TotpData = new();
private bool TotpRequired = false;
private string TotpCode = "";
private async Task DoLogin()
{
try
{
Email = Email.ToLower().Trim();
TotpRequired = await UserService.CheckTotp(Email, Password);
LoginData.Email = LoginData.Email.ToLower().Trim();
if (!TotpRequired)
if (string.IsNullOrEmpty(TotpData.Code))
{
var token = await UserService.Login(Email, Password);
TotpRequired = await UserService.CheckTotp(LoginData.Email, LoginData.Password);
if (!TotpRequired)
{
var token = await UserService.Login(LoginData.Email, LoginData.Password);
await CookieService.SetValue("token", token, 10);
if (NavigationManager.Uri.EndsWith("login"))
NavigationManager.NavigateTo("/", true);
else
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
else
{
await InvokeAsync(StateHasChanged);
}
}
else
{
var token = await UserService.Login(LoginData.Email, LoginData.Password, TotpData.Code);
await CookieService.SetValue("token", token, 10);
if(NavigationManager.Uri.EndsWith("login"))
if (NavigationManager.Uri.EndsWith("login"))
NavigationManager.NavigateTo("/", true);
else
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
else
{
await InvokeAsync(StateHasChanged);
}
}
catch (DisplayException e)
{
// Reset state
LoginData = new();
TotpData = new();
TotpRequired = false;
await AlertService.Error(
SmartTranslateService.Translate("Error"),
SmartTranslateService.Translate(e.Message)
@ -152,6 +171,11 @@
}
catch (Exception e)
{
// Reset state
LoginData = new();
TotpData = new();
TotpRequired = false;
await AlertService.Error(
SmartTranslateService.Translate("Error"),
SmartTranslateService.Translate("An error occured while logging you in")
@ -167,7 +191,7 @@
var url = await GoogleOAuth2Service.GetUrl();
NavigationManager.NavigateTo(url, true);
}
private async Task DoDiscord()
{
var url = await DiscordOAuth2Service.GetUrl();

View file

@ -0,0 +1,64 @@
@using Moonlight.App.Services
@using Moonlight.App.Models.Forms
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@inject SmartTranslateService SmartTranslateService
@inject IdentityService IdentityService
@inject UserService UserService
@inject UserRepository UserRepository
@inject NavigationManager NavigationManager
<div class="d-flex flex-center">
<div class="card rounded-3 w-md-550px">
<div class="card-body">
<div class="d-flex flex-center flex-column-fluid">
<LazyLoader Load="Load">
<SmartForm Model="Password" OnValidSubmit="DoChange">
<div class="text-center mt-3 mb-11">
<h1 class="text-dark fw-bolder mb-3">
<TL>Change your password</TL>
</h1>
<div class="text-gray-500 fw-semibold fs-6">
<TL>You need to change your password in order to use moonlight</TL>
</div>
</div>
<div class="row g-3 mb-9">
<div class="col-md-9">
<InputText @bind-Value="Password.Password" type="password" placeholder="@(SmartTranslateService.Translate("New password"))" class="form-control bg-transparent"/>
</div>
<div class="col">
<button type="submit" class="btn btn-primary float-end">
<TL>Change</TL>
</button>
</div>
</div>
</SmartForm>
</LazyLoader>
</div>
</div>
</div>
</div>
@code {
private PasswordModel Password = new();
private User User;
private async Task Load(LazyLoader loader)
{
User = await IdentityService.Get();
}
private async Task DoChange()
{
await UserService.ChangePassword(User, Password.Password);
User.Status = UserStatus.Unverified;
UserRepository.Update(User);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}

View file

@ -0,0 +1,67 @@
@using Microsoft.AspNetCore.Components
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.App.Services.Sessions
@inject IdentityService IdentityService
@inject UserRepository UserRepository
@inject SmartTranslateService SmartTranslateService
@inject NavigationManager NavigationManager
<div class="d-flex flex-center">
<div class="card rounded-3 w-md-550px">
<div class="card-body">
<div class="d-flex flex-center flex-column-fluid">
<LazyLoader Load="Load">
<SmartForm Model="Name" OnValidSubmit="SetName">
<div class="text-center mt-3 mb-11">
<h1 class="text-dark fw-bolder mb-3">
<TL>Enter your information</TL>
</h1>
<div class="text-gray-500 fw-semibold fs-6">
<TL>You need to enter your full name in order to use moonlight</TL>
</div>
</div>
<div class="row g-3">
<div class="col">
<InputText @bind-Value="Name.FirstName" type="text" placeholder="@(SmartTranslateService.Translate("First name"))" class="form-control bg-transparent"/>
</div>
<div class="col">
<InputText @bind-Value="Name.LastName" type="text" placeholder="@(SmartTranslateService.Translate("Last name"))" class="form-control bg-transparent"/>
</div>
</div>
<button type="submit" class="btn btn-primary float-end mt-3">
<TL>Change</TL>
</button>
</SmartForm>
</LazyLoader>
</div>
</div>
</div>
</div>
@code {
private User User;
private NameModel Name = new ();
private async Task Load(LazyLoader loader)
{
User = await IdentityService.Get();
}
private async Task SetName()
{
User.FirstName = Name.FirstName;
User.LastName = Name.LastName;
User.Status = UserStatus.Unverified;
UserRepository.Update(User);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}

View file

@ -53,7 +53,7 @@ else
{
receivedExceptions.Add(exception);
await ErrorLogService.Log(exception);
await ErrorLogService.Log(exception, x => {});
await base.OnErrorAsync(exception);
}

View file

@ -3,27 +3,30 @@
@using Moonlight.Shared.Components.Partials
@inject SmartTranslateService TranslationService
@inject IJSRuntime JsRuntime
<div class="card-body">
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
</div>
@if (!HideControls)
{
<div class="card-footer pt-0">
<div class="btn-group">
<WButton
Text="@(TranslationService.Translate("Save"))"
WorkingText="@(TranslationService.Translate("Saving"))"
OnClick="Submit"></WButton>
<WButton
CssClasses="btn-danger"
Text="@(TranslationService.Translate("Cancel"))"
WorkingText="@(TranslationService.Translate("Canceling"))"
OnClick="Cancel"></WButton>
</div>
<div class="card bg-black rounded">
<div class="card-body">
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
</div>
}
@if (!HideControls)
{
<div class="card-footer">
<div class="btn-group">
<WButton
Text="@(TranslationService.Translate("Save"))"
WorkingText="@(TranslationService.Translate("Saving"))"
OnClick="Submit"></WButton>
<WButton
CssClasses="btn-danger"
Text="@(TranslationService.Translate("Cancel"))"
WorkingText="@(TranslationService.Translate("Canceling"))"
OnClick="Cancel"></WButton>
</div>
</div>
}
</div>
@code
{
@ -53,8 +56,8 @@
{
AutomaticLayout = true,
Language = "plaintext",
Value = "Wird geladen",
Theme = "vs-dark",
Value = "Loading content",
Theme = "moonlight-theme",
Contextmenu = false,
Minimap = new()
{
@ -68,6 +71,8 @@
{
if (firstRender)
{
await JsRuntime.InvokeVoidAsync("initMonacoTheme");
Editor.OnDidInit = new EventCallback<MonacoEditorBase>(this, async () =>
{
EditorOptions.Language = Language;

View file

@ -1,529 +1,329 @@
@using Moonlight.App.Helpers
@using BlazorContextMenu
@using BlazorDownloadFile
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Helpers
@using Logging.Net
@using Moonlight.App.Models.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using BlazorDownloadFile
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject SmartTranslateService TranslationService
@inject AlertService AlertService
@inject SmartTranslateService SmartTranslateService
@inject IBlazorDownloadFileService FileService
<div class="card card-flush">
@if (Editing == null)
@if (Editing)
{
<div class="card-header pt-8">
<div class="card-title">
<div class="d-flex align-items-center position-relative my-1">
<span class="svg-icon svg-icon-1 position-absolute ms-6">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor"></rect>
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor"></path>
</svg>
</span>
<input type="text" @bind="Search" @bind:event="oninput" class="form-control form-control-solid w-250px ps-15" placeholder="@(TranslationService.Translate("Search files and folders"))">
</div>
</div>
<div class="card-toolbar">
@if (SelectedFiles.Count == 0)
{
<div class="d-flex justify-content-end">
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<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>
<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>
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 pt-5 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<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.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
</div>
}
else
{
<div class="d-flex justify-content-end align-items-center">
<div class="fw-bold me-5">
<span class="me-2">
@(SelectedFiles.Count)
</span>
<TL>Selected</TL>
</div>
<button type="button" class="btn btn-primary me-3">
<TL>Move deleted</TL>
</button>
<button type="button" class="btn btn-danger">
<TL>Delete selected</TL>
</button>
</div>
}
</div>
</div>
<div class="card-body">
<div class="d-flex flex-stack">
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
</div>
<div class="dt-bootstrap4 no-footer">
@if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<div class="table-responsive">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2">
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
<input class="form-check-input" type="checkbox" @onchange="OnAllFileToggle">
</div>
</th>
<th class="min-w-250px">
<TL>File name</TL>
</th>
<th class="min-w-10px">
<TL>File size</TL>
</th>
<th class="min-w-125px">
<TL>Last modified</TL>
</th>
<th class="w-125px"></th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-600">
@foreach (var obj in Objects.Where(x => x.Name.Contains(Search)))
{
<tr class="odd">
<td>
<div class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" @onchange="(e) => OnFileToggle(e, obj)">
</div>
</td>
<td>
<div class="d-flex align-items-center">
@if (obj.IsFile)
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M19 22H5C4.4 22 4 21.6 4 21V3C4 2.4 4.4 2 5 2H14L20 8V21C20 21.6 19.6 22 19 22Z" fill="currentColor"></path>
<path d="M15 8H20L14 2V7C14 7.6 14.4 8 15 8Z" fill="currentColor"></path>
</svg>
</span>
}
else
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<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="M9.2 3H3C2.4 3 2 3.4 2 4V19C2 19.6 2.4 20 3 20H21C21.6 20 22 19.6 22 19V7C22 6.4 21.6 6 21 6H12L10.4 3.60001C10.2 3.20001 9.7 3 9.2 3Z" fill="currentColor"></path>
</svg>
</span>
}
@if (obj.IsFile)
{
<a href="#" @onclick:preventDefault @onclick="() => OpenFile(obj)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
else
{
<a href="#" @onclick:preventDefault @onclick="() => CdPath(obj.Name)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
</div>
</td>
<td>
@(Formatter.FormatSize(obj.Size))
</td>
<td>
@(obj.UpdatedAt.ToShortDateString()) @(obj.UpdatedAt.ToShortTimeString())
</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="obj">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
<FileEditor @ref="Editor"
InitialData="@EditorInitialData"
Language="@EditorLanguage"
OnCancel="() => Cancel()"
OnSubmit="(_) => Cancel(true)"
HideControls="false">
</FileEditor>
}
else
{
if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<FileEditor OnSubmit="SaveFile" OnCancel="CloseFile" InitialData="@(InitialEditorData)" Language="@(Language)"></FileEditor>
}
}
</div>
<div class="card mb-7">
<div class="card-header">
<div class="card-title">
<div class="d-flex flex-stack">
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged" />
</div>
</div>
<div class="card-toolbar">
<div class="d-flex justify-content-end align-items-center">
@if (View != null && View.SelectedFiles.Any())
{
<div class="fw-bold me-5">
<span class="me-2">@(View.SelectedFiles.Length) <TL>selected</TL></span>
</div>
<WButton Text="@(SmartTranslateService.Translate("Move"))"
WorkingText="@(SmartTranslateService.Translate("Moving"))"
CssClasses="btn-primary me-3"
OnClick="StartMoveFiles">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Compress"))"
WorkingText="@(SmartTranslateService.Translate("Compressing"))"
CssClasses="btn-primary me-3"
OnClick="CompressMultiple">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-danger"
OnClick="DeleteMultiple">
</WButton>
}
else
{
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<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>
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
<Item Id="rename" OnClick="OnContextMenuClick"><TL>Rename</TL></Item>
<Item Id="move" OnClick="OnContextMenuClick"><TL>Move</TL></Item>
<Item Id="archive" OnClick="OnContextMenuClick"><TL>Archive</TL></Item>
<Item Id="unarchive" OnClick="OnContextMenuClick"><TL>Unarchive</TL></Item>
<Item Id="download" OnClick="OnContextMenuClick"><TL>Download</TL></Item>
<Item Id="delete" OnClick="OnContextMenuClick"><TL>Delete</TL></Item>
</ContextMenu>
<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" />
}
</div>
</div>
</div>
</div>
<div class="card card-body">
<FileView @ref="View"
Access="Access"
ContextActions="Actions"
OnSelectionChanged="OnSelectionChanged"
OnElementClicked="OnElementClicked"
DisableScrolling="true">
</FileView>
</div>
<FileSelectModal @ref="FileSelectModal"
OnlyFolder="true"
Title="@(SmartTranslateService.Translate("Select folder to move the file(s) to"))"
Access="MoveAccess"
OnSubmit="OnFileMoveSubmit">
</FileSelectModal>
}
@code
{
[Parameter]
public IFileAccess FileAccess { get; set; }
public FileAccess Access { get; set; }
// Data
// File Editor
private bool Editing = false;
private string EditorInitialData = "";
private string EditorLanguage = "";
private FileData EditingFile;
private FileEditor Editor;
private List<FileManagerObject> SelectedFiles { get; set; } = new();
private List<FileManagerObject> Objects { get; set; } = new();
private string CurrentPath = "";
// File View
private FileView? View;
// Search
private string SearchValue = "";
// File Move
private FileAccess MoveAccess;
private FileSelectModal FileSelectModal;
private FileData? SingleMoveFile = null;
// Config
private ContextAction[] Actions = Array.Empty<ContextAction>();
private string Search
protected override void OnInitialized()
{
get { return SearchValue; }
set
MoveAccess = (FileAccess)Access.Clone();
List<ContextAction> actions = new();
actions.Add(new()
{
SearchValue = value;
InvokeAsync(StateHasChanged);
}
}
Id = "rename",
Name = "Rename",
Action = async (x) =>
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Rename"),
SmartTranslateService.Translate("Enter a new name"),
x.Name
);
// States
private bool Loading = false;
// States - Editor
private FileManagerObject? Editing = null;
private string InitialEditorData = "";
private string Language = "plaintext";
// States - File Upload
private bool Uploading = false;
private int Percent = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
if (name != x.Name)
{
await Access.Move(x, Access.CurrentPath + name);
}
await View!.Refresh();
}
});
actions.Add(new ()
{
await RefreshActive();
}
}
private async Task RefreshActive()
{
Loading = true;
await InvokeAsync(StateHasChanged);
await Refresh(false);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async Task Refresh(bool rerender = true)
{
SelectedFiles.Clear();
Objects.Clear();
CurrentPath = await FileAccess.GetCurrentPath();
var data = await FileAccess.GetDirectoryContent();
Objects = data.ToList();
if (rerender)
await InvokeAsync(StateHasChanged);
}
private async Task CdPath(string path)
{
await FileAccess.ChangeDirectory(path);
await RefreshActive();
}
private async Task SetPath(string path)
{
await FileAccess.SetDirectory(path);
await RefreshActive();
}
private async Task OnContextMenuClick(ItemClickEventArgs e)
{
var data = e.Data as FileManagerObject;
if (data == null)
return;
switch (e.MenuItem.Id)
{
case "delete":
await FileAccess.Delete(data);
break;
case "download":
if (data.IsFile)
Id = "download",
Name = "Download",
Action = async (x) =>
{
if (x.IsFile)
{
try
{
var stream = await FileAccess.GetDownloadStream(data);
await ToastService.Info(TranslationService.Translate("Starting download"));
var stream = await Access.DownloadStream(x);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
await FileService.AddBuffer(stream);
await FileService.DownloadBinaryBuffers(data.Name);
await FileService.DownloadBinaryBuffers(x.Name);
}
catch (NotImplementedException)
{
try
{
var url = await FileAccess.GetDownloadUrl(data);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(TranslationService.Translate("Starting download"));
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file");
Logger.Error(exception.Message);
}
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file stream");
Logger.Error(exception.Message);
var url = await Access.DownloadUrl(x);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
}
}
break;
case "rename":
var newName = await AlertService.Text(TranslationService.Translate("Rename"), TranslationService.Translate("Enter a new name"), data.Name);
var path = await FileAccess.GetCurrentPath();
await FileAccess.Move(data, path + "/" + newName);
break;
}
await Refresh(false);
}
private async Task OnFileToggle(ChangeEventArgs obj, FileManagerObject o)
{
if ((bool)obj.Value)
{
if (SelectedFiles.Contains(o))
return;
SelectedFiles.Add(o);
await InvokeAsync(StateHasChanged);
}
else
{
if (!SelectedFiles.Contains(o))
return;
SelectedFiles.Remove(o);
await InvokeAsync(StateHasChanged);
}
}
private async Task OnAllFileToggle(ChangeEventArgs obj)
{
if ((bool)obj.Value)
{
foreach (var o in Objects)
{
if (SelectedFiles.Contains(o))
continue;
SelectedFiles.Add(o);
}
await InvokeAsync(StateHasChanged);
}
else
});
actions.Add(new()
{
foreach (var o in Objects)
Id = "compress",
Name = "Compress",
Action = async (x) =>
{
if (!SelectedFiles.Contains(o))
continue;
SelectedFiles.Remove(o);
await Access.Compress(x);
await View!.Refresh();
}
});
actions.Add(new ()
{
Id = "decompress",
Name = "Decompress",
Action = async (x) =>
{
await Access.Decompress(x);
await View!.Refresh();
}
});
actions.Add(new()
{
Id = "move",
Name = "Move",
Action = async (x) =>
{
SingleMoveFile = x;
await StartMoveFiles();
}
});
actions.Add(new()
{
Id = "delete",
Name = "Delete",
Action = async (x) =>
{
await Access.Delete(x);
await View!.Refresh();
}
});
Actions = actions.ToArray();
}
private async Task<bool> OnElementClicked(FileData fileData)
{
if (fileData.IsFile)
{
EditorInitialData = await Access.Read(fileData);
EditorLanguage = MonacoTypeHelper.GetEditorType(fileData.Name);
EditingFile = fileData;
Editing = true;
await InvokeAsync(StateHasChanged);
return true;
}
}
private async Task CreateFolder()
{
var name = await AlertService.Text(TranslationService.Translate("Create a new folder"), TranslationService.Translate("Enter a name"), "");
if (string.IsNullOrEmpty(name))
return;
await FileAccess.CreateDirectory(name);
await Refresh();
}
private async void SaveFile(string data)
{
if (Editing == null)
return;
await FileAccess.WriteFile(Editing, data);
Editing = null;
await Refresh();
}
private async Task OpenFile(FileManagerObject o)
{
Editing = o;
Loading = true;
await InvokeAsync(StateHasChanged);
InitialEditorData = await FileAccess.ReadFile(Editing);
Language = MonacoTypeHelper.GetEditorType(Editing.Name);
Loading = false;
await InvokeAsync(StateHasChanged);
return false;
}
private async void CloseFile()
private async void Cancel(bool save = false)
{
Editing = null;
if (save)
{
var data = await Editor.GetData();
await Access.Write(EditingFile, data);
}
Editing = false;
await InvokeAsync(StateHasChanged);
}
private async Task Launch()
{
NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl());
var url = await Access.GetLaunchUrl();
NavigationManager.NavigateTo(url, true);
}
private async Task OnFileChanged(InputFileChangeEventArgs arg)
private async Task CreateFolder()
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Create a new folder"),
SmartTranslateService.Translate("Enter a name"),
""
);
if (string.IsNullOrEmpty(name))
return;
await Access.MkDir(name);
await View!.Refresh();
}
private async Task OnSelectionChanged()
{
Uploading = true;
await InvokeAsync(StateHasChanged);
}
foreach (var browserFile in arg.GetMultipleFiles())
private async Task StartMoveFiles()
{
await FileSelectModal.Show();
}
private async Task DeleteMultiple()
{
foreach (var data in View!.SelectedFiles)
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await FileAccess.UploadFile(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
await Refresh();
}
catch (Exception e)
{
await ToastService.Error(TranslationService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
}
else
{
await ToastService.Error(TranslationService.Translate("The uploaded file should not be bigger than 100MB"));
}
await Access.Delete(data);
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await View!.Refresh();
}
await ToastService.Success(TranslationService.Translate("File upload complete"));
private async Task CompressMultiple()
{
await Access.Compress(View!.SelectedFiles);
await View!.Refresh();
}
private async Task OnFileMoveSubmit(string path)
{
foreach (var sFile in View!.SelectedFiles)
{
await Access.Move(sFile, path + sFile.Name);
}
if (SingleMoveFile != null)
{
await Access.Move(SingleMoveFile, path + SingleMoveFile.Name);
SingleMoveFile = null;
}
await View.Refresh();
}
// This method can be called by every component to refresh the view
private async Task OnComponentStateChanged()
{
await View!.Refresh();
await InvokeAsync(StateHasChanged);
}
}

View file

@ -0,0 +1,529 @@
@using Moonlight.App.Helpers
@using BlazorContextMenu
@using BlazorDownloadFile
@using Logging.Net
@using Moonlight.App.Models.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject SmartTranslateService TranslationService
@inject IBlazorDownloadFileService FileService
<div class="card card-flush">
@if (Editing == null)
{
<div class="card-header pt-8">
<div class="card-title">
<div class="d-flex align-items-center position-relative my-1">
<span class="svg-icon svg-icon-1 position-absolute ms-6">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor"></rect>
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor"></path>
</svg>
</span>
<input type="text" @bind="Search" @bind:event="oninput" class="form-control form-control-solid w-250px ps-15" placeholder="@(TranslationService.Translate("Search files and folders"))">
</div>
</div>
<div class="card-toolbar">
@if (SelectedFiles.Count == 0)
{
<div class="d-flex justify-content-end">
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<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>
<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>
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 pt-5 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<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.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
</div>
}
else
{
<div class="d-flex justify-content-end align-items-center">
<div class="fw-bold me-5">
<span class="me-2">
@(SelectedFiles.Count)
</span>
<TL>Selected</TL>
</div>
<button type="button" class="btn btn-primary me-3">
<TL>Move deleted</TL>
</button>
<button type="button" class="btn btn-danger">
<TL>Delete selected</TL>
</button>
</div>
}
</div>
</div>
<div class="card-body">
<div class="d-flex flex-stack">
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
</div>
<div class="dt-bootstrap4 no-footer">
@if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<div class="table-responsive">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2">
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
<input class="form-check-input" type="checkbox" @onchange="OnAllFileToggle">
</div>
</th>
<th class="min-w-250px">
<TL>File name</TL>
</th>
<th class="min-w-10px">
<TL>File size</TL>
</th>
<th class="min-w-125px">
<TL>Last modified</TL>
</th>
<th class="w-125px"></th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-600">
@foreach (var obj in Objects.Where(x => x.Name.Contains(Search)))
{
<tr class="odd">
<td>
<div class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" @onchange="(e) => OnFileToggle(e, obj)">
</div>
</td>
<td>
<div class="d-flex align-items-center">
@if (obj.IsFile)
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M19 22H5C4.4 22 4 21.6 4 21V3C4 2.4 4.4 2 5 2H14L20 8V21C20 21.6 19.6 22 19 22Z" fill="currentColor"></path>
<path d="M15 8H20L14 2V7C14 7.6 14.4 8 15 8Z" fill="currentColor"></path>
</svg>
</span>
}
else
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<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="M9.2 3H3C2.4 3 2 3.4 2 4V19C2 19.6 2.4 20 3 20H21C21.6 20 22 19.6 22 19V7C22 6.4 21.6 6 21 6H12L10.4 3.60001C10.2 3.20001 9.7 3 9.2 3Z" fill="currentColor"></path>
</svg>
</span>
}
@if (obj.IsFile)
{
<a href="#" @onclick:preventDefault @onclick="() => OpenFile(obj)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
else
{
<a href="#" @onclick:preventDefault @onclick="() => CdPath(obj.Name)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
</div>
</td>
<td>
@(Formatter.FormatSize(obj.Size))
</td>
<td>
@(obj.UpdatedAt.ToShortDateString()) @(obj.UpdatedAt.ToShortTimeString())
</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="obj">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
}
else
{
if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<FileEditor OnSubmit="SaveFile" OnCancel="CloseFile" InitialData="@(InitialEditorData)" Language="@(Language)"></FileEditor>
}
}
</div>
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
<Item Id="rename" OnClick="OnContextMenuClick"><TL>Rename</TL></Item>
<Item Id="move" OnClick="OnContextMenuClick"><TL>Move</TL></Item>
<Item Id="archive" OnClick="OnContextMenuClick"><TL>Archive</TL></Item>
<Item Id="unarchive" OnClick="OnContextMenuClick"><TL>Unarchive</TL></Item>
<Item Id="download" OnClick="OnContextMenuClick"><TL>Download</TL></Item>
<Item Id="delete" OnClick="OnContextMenuClick"><TL>Delete</TL></Item>
</ContextMenu>
@code
{
[Parameter]
public IFileAccess FileAccess { get; set; }
// Data
private List<FileManagerObject> SelectedFiles { get; set; } = new();
private List<FileManagerObject> Objects { get; set; } = new();
private string CurrentPath = "";
// Search
private string SearchValue = "";
private string Search
{
get { return SearchValue; }
set
{
SearchValue = value;
InvokeAsync(StateHasChanged);
}
}
// States
private bool Loading = false;
// States - Editor
private FileManagerObject? Editing = null;
private string InitialEditorData = "";
private string Language = "plaintext";
// States - File Upload
private bool Uploading = false;
private int Percent = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await RefreshActive();
}
}
private async Task RefreshActive()
{
Loading = true;
await InvokeAsync(StateHasChanged);
await Refresh(false);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async Task Refresh(bool rerender = true)
{
SelectedFiles.Clear();
Objects.Clear();
CurrentPath = await FileAccess.GetCurrentPath();
var data = await FileAccess.GetDirectoryContent();
Objects = data.ToList();
if (rerender)
await InvokeAsync(StateHasChanged);
}
private async Task CdPath(string path)
{
await FileAccess.ChangeDirectory(path);
await RefreshActive();
}
private async Task SetPath(string path)
{
await FileAccess.SetDirectory(path);
await RefreshActive();
}
private async Task OnContextMenuClick(ItemClickEventArgs e)
{
var data = e.Data as FileManagerObject;
if (data == null)
return;
switch (e.MenuItem.Id)
{
case "delete":
await FileAccess.Delete(data);
break;
case "download":
if (data.IsFile)
{
try
{
var stream = await FileAccess.GetDownloadStream(data);
await ToastService.Info(TranslationService.Translate("Starting download"));
await FileService.AddBuffer(stream);
await FileService.DownloadBinaryBuffers(data.Name);
}
catch (NotImplementedException)
{
try
{
var url = await FileAccess.GetDownloadUrl(data);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(TranslationService.Translate("Starting download"));
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file");
Logger.Error(exception.Message);
}
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file stream");
Logger.Error(exception.Message);
}
}
break;
case "rename":
var newName = await AlertService.Text(TranslationService.Translate("Rename"), TranslationService.Translate("Enter a new name"), data.Name);
var path = await FileAccess.GetCurrentPath();
await FileAccess.Move(data, path + "/" + newName);
break;
}
await Refresh(false);
}
private async Task OnFileToggle(ChangeEventArgs obj, FileManagerObject o)
{
if ((bool)obj.Value)
{
if (SelectedFiles.Contains(o))
return;
SelectedFiles.Add(o);
await InvokeAsync(StateHasChanged);
}
else
{
if (!SelectedFiles.Contains(o))
return;
SelectedFiles.Remove(o);
await InvokeAsync(StateHasChanged);
}
}
private async Task OnAllFileToggle(ChangeEventArgs obj)
{
if ((bool)obj.Value)
{
foreach (var o in Objects)
{
if (SelectedFiles.Contains(o))
continue;
SelectedFiles.Add(o);
}
await InvokeAsync(StateHasChanged);
}
else
{
foreach (var o in Objects)
{
if (!SelectedFiles.Contains(o))
continue;
SelectedFiles.Remove(o);
}
await InvokeAsync(StateHasChanged);
}
}
private async Task CreateFolder()
{
var name = await AlertService.Text(TranslationService.Translate("Create a new folder"), TranslationService.Translate("Enter a name"), "");
if (string.IsNullOrEmpty(name))
return;
await FileAccess.CreateDirectory(name);
await Refresh();
}
private async void SaveFile(string data)
{
if (Editing == null)
return;
await FileAccess.WriteFile(Editing, data);
Editing = null;
await Refresh();
}
private async Task OpenFile(FileManagerObject o)
{
Editing = o;
Loading = true;
await InvokeAsync(StateHasChanged);
InitialEditorData = await FileAccess.ReadFile(Editing);
Language = MonacoTypeHelper.GetEditorType(Editing.Name);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async void CloseFile()
{
Editing = null;
await InvokeAsync(StateHasChanged);
}
private async Task Launch()
{
NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl());
}
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
Uploading = true;
await InvokeAsync(StateHasChanged);
foreach (var browserFile in arg.GetMultipleFiles())
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await FileAccess.UploadFile(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
await Refresh();
}
catch (Exception e)
{
await ToastService.Error(TranslationService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
}
else
{
await ToastService.Error(TranslationService.Translate("The uploaded file should not be bigger than 100MB"));
}
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await ToastService.Success(TranslationService.Translate("File upload complete"));
}
}

View file

@ -0,0 +1,47 @@
@using Moonlight.App.Helpers.Files
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = Access.CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<Task>? OnPathChanged { get; set; }
public async Task SetPath(string path)
{
await Access.SetDir(path);
OnPathChanged?.Invoke();
}
}

View file

@ -0,0 +1,132 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject ModalService ModalService
@inject SmartTranslateService SmartTranslateService
<div class="modal" id="fileView@(Id)" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@(Title)
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<FileView @ref="FileView"
Access="Access"
HideSelect="true"
Filter="DoFilter"
OnElementClicked="OnElementClicked">
</FileView>
</div>
<div class="modal-footer">
<WButton Text="@(SmartTranslateService.Translate("Submit"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-primary"
OnClick="Submit">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-danger"
OnClick="Cancel">
</WButton>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public bool OnlyFolder { get; set; } = false;
[Parameter]
public Func<FileData, bool>? Filter { get; set; }
[Parameter]
public string Title { get; set; } = "Select file or folder";
[Parameter]
public Func<string, Task>? OnSubmit { get; set; }
[Parameter]
public Func<Task>? OnCancel { get; set; }
private int Id = 0;
private string Result = "/";
private FileView FileView;
protected override void OnInitialized()
{
Id = this.GetHashCode();
}
public async Task Show()
{
// Reset
Result = "/";
await Access.SetDir("/");
await FileView.Refresh();
await ModalService.Show("fileView" + Id);
}
public async Task Hide()
{
await Cancel();
}
private async Task Cancel()
{
await ModalService.Hide("fileView" + Id);
await OnCancel?.Invoke()!;
}
private async Task Submit()
{
await ModalService.Hide("fileView" + Id);
await OnSubmit?.Invoke(Result)!;
}
private bool DoFilter(FileData file)
{
if (OnlyFolder)
{
if (file.IsFile)
return false;
else
{
if (Filter != null)
return Filter.Invoke(file);
else
return true;
}
}
else
{
if (Filter != null)
return Filter.Invoke(file);
else
return true;
}
}
private async Task<bool> OnElementClicked(FileData file)
{
Result = Access.CurrentPath + file.Name + (file.IsFile ? "" : "/");
if (!OnlyFolder && file.IsFile)
{
await Submit();
}
return false;
}
}

View file

@ -0,0 +1,90 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Logging.Net
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<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.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<Task> OnUploadComplete { get; set; }
private bool Uploading = false;
private int Percent = 0;
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
await ToastService.CreateProcessToast("upload", SmartTranslateService.Translate("Uploading files"));
Uploading = true;
await InvokeAsync(StateHasChanged);
int i = 1;
foreach (var browserFile in arg.GetMultipleFiles())
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await Access.Upload(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
OnUploadComplete?.Invoke();
}
catch (Exception e)
{
await ToastService.Error(SmartTranslateService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
await ToastService.UpdateProcessToast("upload", $"{i}/{arg.GetMultipleFiles().Count} {SmartTranslateService.Translate("complete")}");
}
else
{
await ToastService.Error(SmartTranslateService.Translate("The uploaded file should not be bigger than 100MB"));
}
i++;
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await ToastService.UpdateProcessToast("upload", SmartTranslateService.Translate("Upload complete"));
await ToastService.RemoveProcessToast("upload");
}
}

View file

@ -0,0 +1,276 @@
@using Moonlight.App.Helpers.Files
@using Logging.Net
@using BlazorContextMenu
@using Moonlight.App.Helpers
<div class="table-responsive">
<div class="dataTables_scroll">
<div class="dataTables_scrollHead">
<div class="dataTables_scrollHeadInner">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2 sorting_disabled">
@if (!HideSelect)
{
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
@if (AllToggled)
{
<input @onclick="() => SetToggleState(false)" class="form-check-input" type="checkbox" checked="">
}
else
{
<input @onclick="() => SetToggleState(true)" class="form-check-input" type="checkbox">
}
</div>
}
</th>
<th class="min-w-250px sorting_disabled">Name</th>
</tr>
</thead>
</table>
</div>
</div>
<div class="dataTables_scrollBody" style="@(DisableScrolling ? "" : "position: relative; overflow: auto; max-height: 700px; width: 100%;")">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer" style="width: 100%;">
<tbody class="fw-semibold text-gray-600">
<LazyLoader Load="Load">
<tr class="even">
<td class="w-10px">
</td>
<td>
<div class="d-flex align-items-center">
<span class="icon-wrapper">
<i class="bx bx-md bx-up-arrow-alt text-primary"></i>
</span>
<a href="#" @onclick:preventDefault @onclick="GoUp" class="ms-3 text-gray-800 text-hover-primary">
<TL>Go up</TL>
</a>
</div>
</td>
<td></td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
</div>
</div>
</td>
</tr>
@foreach (var file in Data)
{
<tr class="even">
<td class="w-10px">
@if (!HideSelect)
{
<div class="form-check form-check-sm form-check-custom form-check-solid">
@{
var toggle = ToggleStatusCache.ContainsKey(file) && ToggleStatusCache[file];
}
@if (toggle)
{
<input @onclick="() => SetToggleState(file, false)" class="form-check-input" type="checkbox" checked="checked">
}
else
{
<input @onclick="() => SetToggleState(file, true)" class="form-check-input" type="checkbox">
}
</div>
}
</td>
<td>
<div class="d-flex align-items-center">
<span class="icon-wrapper">
@if (file.IsFile)
{
<i class="bx bx-md bx-file text-primary"></i>
}
else
{
<i class="bx bx-md bx-folder text-primary"></i>
}
</span>
<a href="#" @onclick:preventDefault @onclick="() => OnClicked(file)" class="ms-3 text-gray-800 text-hover-primary">@(file.Name)</a>
</div>
</td>
<td>@(Formatter.FormatSize(file.Size))</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2 me-7">
@if (ContextActions.Any())
{
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="file">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
}
</div>
</div>
</td>
</tr>
}
</LazyLoader>
</tbody>
</table>
</div>
</div>
</div>
@if (ContextActions.Any())
{
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
@foreach (var action in ContextActions)
{
<Item Id="@action.Id" OnClick="OnContextMenuClick">
<TL>@action.Name</TL>
</Item>
}
</ContextMenu>
}
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<FileData, Task<bool>>? OnElementClicked { get; set; }
[Parameter]
public Func<Task>? OnSelectionChanged { get; set; }
[Parameter]
public ContextAction[] ContextActions { get; set; } = Array.Empty<ContextAction>();
[Parameter]
public bool HideSelect { get; set; } = false;
[Parameter]
public bool DisableScrolling { get; set; } = false;
[Parameter]
public Func<FileData, bool>? Filter { get; set; }
public FileData[] SelectedFiles => ToggleStatusCache
.Where(x => x.Value)
.Select(x => x.Key)
.ToArray();
private FileData[] Data = Array.Empty<FileData>();
private Dictionary<FileData, bool> ToggleStatusCache = new();
private bool AllToggled = false;
public async Task Refresh()
{
var list = new List<FileData>();
foreach (var fileData in await Access.Ls())
{
if (Filter != null)
{
if(Filter.Invoke(fileData))
list.Add(fileData);
}
else
list.Add(fileData);
}
Data = list.ToArray();
ToggleStatusCache.Clear();
AllToggled = false;
foreach (var fileData in Data)
{
ToggleStatusCache.Add(fileData, false);
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task Load(LazyLoader arg)
{
await Refresh();
}
private async Task SetToggleState(FileData fileData, bool status)
{
if (ToggleStatusCache.ContainsKey(fileData))
ToggleStatusCache[fileData] = status;
else
ToggleStatusCache.Add(fileData, status);
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task SetToggleState(bool status)
{
AllToggled = status;
foreach (var fd in ToggleStatusCache.Keys)
{
ToggleStatusCache[fd] = status;
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task OnClicked(FileData fileData)
{
if (OnElementClicked != null)
{
var canceled = await OnElementClicked.Invoke(fileData);
if (canceled)
return;
}
if (!fileData.IsFile)
{
await Access.Cd(fileData.Name);
await Refresh();
}
}
private async Task GoUp()
{
if (OnElementClicked != null)
{
var canceled = await OnElementClicked.Invoke(new()
{
Name = "..",
IsFile = false
});
if (canceled)
return;
}
await Access.Up();
await Refresh();
}
private Task OnContextMenuClick(ItemClickEventArgs eventArgs)
{
var action = ContextActions.FirstOrDefault(x => x.Id == eventArgs.MenuItem.Id);
if (action != null)
{
action.Action.Invoke((FileData)eventArgs.Data);
}
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,57 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject SmartTranslateService SmartTranslateService
@inject AlertService AlertService
@if (!Working)
{
<button class="btn btn-danger" @onclick="Do">
<i class="bx bx-trash"></i>
</button>
}
else
{
<button class="btn btn-danger disabled" disabled="">
<span class="spinner-border spinner-border-sm align-middle me-2"></span>
</button>
}
@code
{
private bool Working { get; set; } = false;
[Parameter]
public Func<Task>? OnClick { get; set; }
[Parameter]
public bool Confirm { get; set; } = false;
private async Task Do()
{
Working = true;
StateHasChanged();
await Task.Run(async () =>
{
if (Confirm)
{
var b = await AlertService.YesNo(
SmartTranslateService.Translate("Are you sure?"),
SmartTranslateService.Translate("Do you really want to delete it?"),
SmartTranslateService.Translate("Yes"),
SmartTranslateService.Translate("No")
);
if (b)
{
if(OnClick != null)
await OnClick.Invoke();
}
}
Working = false;
await InvokeAsync(StateHasChanged);
});
}
}

View file

@ -0,0 +1,102 @@
@using Moonlight.App.Helpers
<div class="form">
<EditForm @ref="EditForm" Model="Model" OnValidSubmit="ValidSubmit" OnInvalidSubmit="InvalidSubmit">
<DataAnnotationsValidator></DataAnnotationsValidator>
@if (Working)
{
<div class="d-flex flex-center flex-column">
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
<span class="mt-3 fs-5"><TL>Proccessing</TL></span>
</div>
}
else
{
if (ErrorMessages.Any())
{
<div class="alert alert-danger p-10 mb-3">
@foreach (var msg in ErrorMessages)
{
<TL>@(msg)</TL>
<br/>
}
</div>
}
@(ChildContent)
}
</EditForm>
</div>
@code
{
[Parameter]
public object Model { get; set; }
[Parameter]
public Func<Task>? OnValidSubmit { get; set; }
[Parameter]
public Func<Task>? OnInvalidSubmit { get; set; }
[Parameter]
public Func<Task>? OnSubmit { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private EditForm EditForm;
private List<string> ErrorMessages = new();
private bool Working = false;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
EditForm.EditContext!.SetFieldCssClassProvider(new FieldCssHelper());
}
}
private async Task ValidSubmit(EditContext context)
{
ErrorMessages.Clear();
Working = true;
await InvokeAsync(StateHasChanged);
await Task.Run(async () =>
{
await InvokeAsync(async () =>
{
if (OnValidSubmit != null)
await OnValidSubmit.Invoke();
if (OnSubmit != null)
await OnSubmit.Invoke();
});
Working = false;
await InvokeAsync(StateHasChanged);
});
}
private async Task InvalidSubmit(EditContext context)
{
ErrorMessages.Clear();
context.Validate();
foreach (var message in context.GetValidationMessages())
{
ErrorMessages.Add(message);
}
await InvokeAsync(StateHasChanged);
if (OnInvalidSubmit != null)
await OnInvalidSubmit.Invoke();
if (OnSubmit != null)
await OnSubmit.Invoke();
}
}

View file

@ -0,0 +1,59 @@
@typeparam TField
@inherits InputBase<TField>
<select class="form-select" @bind="Binding">
@foreach(var item in Items)
{
<option value="@(item!.GetHashCode())">@(DisplayField(item))</option>
}
</select>
@code
{
[Parameter]
public TField[] Items { get; set; }
[Parameter]
public Func<TField, string> DisplayField { get; set; }
protected override void OnInitialized()
{
}
protected override string? FormatValueAsString(TField? value)
{
if (value == null)
return null;
return DisplayField.Invoke(value);
}
protected override bool TryParseValueFromString(string? value, out TField result, out string? validationErrorMessage)
{
validationErrorMessage = "";
result = default(TField)!;
return false;
}
private int Binding
{
get
{
if (Value == null)
return -1;
return Value.GetHashCode();
}
set
{
var i = Items.FirstOrDefault(x => x!.GetHashCode() == value);
if (i != null)
{
Value = i;
ValueChanged.InvokeAsync(i);
}
}
}
}

View file

@ -27,12 +27,17 @@
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/profile">
Overview
<TL>Overview</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/subscriptions">
Subscriptions
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/security">
<TL>Security</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/subscriptions">
<TL>Subscriptions</TL>
</a>
</li>
</ul>

View file

@ -9,6 +9,34 @@
@inject NavigationManager NavigationManager
@inject CookieService CookieService
<div class="menu menu-column justify-content-center"
data-kt-menu="true">
<div class="menu-item">
<div class="dropdown">
<button class="btn btn-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
<TL>Create</TL>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item py-2" href="/servers/create">
<TL>Server</TL>
</a>
</li>
<li>
<a class="dropdown-item py-2" href="/domains/create">
<TL>Domain</TL>
</a>
</li>
<li>
<a class="dropdown-item py-2" href="/websites/create">
<TL>Website</TL>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="app-navbar flex-shrink-0">
<div class="app-navbar-item ms-1 ms-lg-3">
<ThemeSwitcher
@ -20,17 +48,11 @@
@if (User != null)
{
<div class="app-navbar-item ms-1 ms-lg-3">
<div class="btn btn-icon btn-custom btn-icon-muted btn-active-light btn-active-color-primary w-35px h-35px w-md-40px h-md-40px position-relative" id="support_ticket_toggle">
<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">
<path opacity="0.3" d="M20 3H4C2.89543 3 2 3.89543 2 5V16C2 17.1046 2.89543 18 4 18H4.5C5.05228 18 5.5 18.4477 5.5 19V21.5052C5.5 22.1441 6.21212 22.5253 6.74376 22.1708L11.4885 19.0077C12.4741 18.3506 13.6321 18 14.8167 18H20C21.1046 18 22 17.1046 22 16V5C22 3.89543 21.1046 3 20 3Z" fill="currentColor"></path>
<rect x="6" y="12" width="7" height="2" rx="1" fill="currentColor"></rect>
<rect x="6" y="7" width="12" height="2" rx="1" fill="currentColor"></rect>
</svg>
</span>
</div>
<a href="/support" class="btn btn-icon btn-custom btn-icon-muted btn-active-light btn-active-color-primary w-35px h-35px w-md-40px h-md-40px position-relative">
<i class="bx bx-support"></i>
</a>
</div>
<div class="app-navbar-item ms-1 ms-lg-3" id="kt_header_user_menu_toggle">
<div class="cursor-pointer symbol symbol-35px symbol-md-40px" data-kt-menu-trigger="click" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
<img alt="Avatar" src="/api/moonlight/avatar/@(User.Id)"/>
@ -45,7 +67,7 @@
<div class="d-flex flex-column">
<div class="fw-bold d-flex align-items-center fs-5">
@(User.FirstName) @(User.LastName)
@if (User.Admin)
{
<span class="badge badge-light-success fw-bold fs-8 px-2 py-1 ms-2">Admin</span>
@ -57,10 +79,14 @@
</div>
<div class="separator my-2"></div>
<div class="menu-item px-5 my-1">
<a href="/profile" class="menu-link px-5"><TL>Profile</TL></a>
<a href="/profile" class="menu-link px-5">
<TL>Profile</TL>
</a>
</div>
<div class="menu-item px-5">
<a @onclick="Logout" class="menu-link px-5"><TL>Logout</TL></a>
<a @onclick="Logout" class="menu-link px-5">
<TL>Logout</TL>
</a>
</div>
</div>
</div>
@ -86,4 +112,4 @@
await CookieService.SetValue("token", "", 1);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}
}

View file

@ -225,6 +225,14 @@ else
<span class="menu-title"><TL>Support</TL></span>
</a>
</div>
<div class="menu-item">
<a class="menu-link" href="/admin/subscriptions">
<span class="menu-icon">
<i class="bx bx-credit-card"></i>
</span>
<span class="menu-title"><TL>Subscriptions</TL></span>
</a>
</div>
<div class="menu-item">
<a class="menu-link" href="/admin/statistics">
<span class="menu-icon">

View file

@ -1,7 +1,7 @@
<div class="alert alert-primary d-flex rounded p-6">
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold"><TL>Plugins</TL></h4>
<h4 class="text-gray-900 fw-bold"><TL>Addons</TL></h4>
<div class="fs-6 text-gray-700 pe-7">
<TL>This feature is currently not available</TL>
</div>

View file

@ -15,21 +15,20 @@
@inject AlertService AlertService
@inject SmartTranslateService TranslationService
<div class="row g-5 g-xl-10 mb-xl-10">
<Terminal @ref="Terminal" RunOnFirstRender="RunOnFirstRender"></Terminal>
<div class="mt-3 row">
<div class="ms-2 input-group">
<div class="card card-body rounded bg-black p-3">
<div class="d-flex flex-column">
<Terminal @ref="Terminal" RunOnFirstRender="RunOnFirstRender"></Terminal>
<div class="input-group">
<script suppress-error="BL9992">
function checkEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById("sendCmd").click();
}
}
</script>
<input @bind="@CommandInput" class="form-control" onkeyup="checkEnter(event)" placeholder="@(TranslationService.Translate("Enter command"))"/>
function checkEnter(event)
{
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById("sendCmd").click();
}
}
</script>
<input @bind="@CommandInput" class="form-control rounded-start" onkeyup="checkEnter(event)" placeholder="@(TranslationService.Translate("Enter command"))"/>
<button id="sendCmd" @onclick="SendCommand" class="input-group-text btn btn-primary">@(TranslationService.Translate("Execute"))</button>
</div>
</div>

View file

@ -1,27 +1,27 @@
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Services
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Files
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerService ServerService
@inject IdentityService IdentityService
@inject WingsApiHelper WingsApiHelper
@inject WingsJwtHelper WingsJwtHelper
@inject ConfigService ConfigService
<LazyLoader Load="Load">
<FileManager FileAccess="FileAccess"></FileManager>
</LazyLoader>
<FileManager Access="FileAccess"></FileManager>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
[CascadingParameter]
public User User { get; set; }
private IFileAccess FileAccess;
private FileAccess FileAccess;
private async Task Load(LazyLoader arg)
protected override void OnInitialized()
{
var user = await IdentityService.Get(); // User for launch url
FileAccess = await ServerService.CreateFileAccess(CurrentServer, user);
FileAccess = new WingsFileAccess(WingsApiHelper, WingsJwtHelper, CurrentServer, ConfigService, User);
}
}

View file

@ -134,10 +134,10 @@
</a>
</li>
<li class="nav-item w-100 me-0 mb-md-2">
<a href="/server/@(CurrentServer.Uuid)/plugins" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
<a href="/server/@(CurrentServer.Uuid)/addons" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
<i class="bx bx-plug bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Plugins</TL></span>
<span class="fs-5"><TL>Addons</TL></span>
</span>
</a>
</li>

View file

@ -1,51 +1,66 @@
@using PteroConsole.NET
@using Moonlight.App.Database.Entities
@using Moonlight.Shared.Components.ServerControl.Settings
@using Microsoft.AspNetCore.Components.Rendering
<div class="row mb-5">
@if (Tags.Contains("paperversion"))
{
<PaperVersionSetting></PaperVersionSetting>
}
@if (Tags.Contains("pythonversion"))
{
<PythonVersionSetting></PythonVersionSetting>
}
@{
/*
* @if (Tags.Contains("pythonfile"))
{
<PythonFileSetting></PythonFileSetting>
}
@if (Tags.Contains("javascriptfile"))
{
<JavascriptFileSetting></JavascriptFileSetting>
}
*/
}
@if (Tags.Contains("javascriptversion"))
{
<JavascriptVersionSetting></JavascriptVersionSetting>
}
@if (Tags.Contains("join2start"))
{
<Join2StartSetting></Join2StartSetting>
}
</div>
<LazyLoader Load="Load">
<div class="accordion" id="serverSetting">
@foreach (var setting in Settings)
{
<div class="accordion-item">
<h2 class="accordion-header" id="serverSetting-header@(setting.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#serverSetting-body@(setting.GetHashCode())" aria-expanded="true" aria-controls="serverSetting-body@(setting.GetHashCode())">
<TL>@(setting.Key)</TL>
</button>
</h2>
<div id="serverSetting-body@(setting.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverSetting-header@(setting.GetHashCode())" data-bs-parent="#serverSetting">
<div class="accordion-body">
@(GetComponent(setting.Value))
</div>
</div>
</div>
}
</div>
</LazyLoader>
@code
{
[CascadingParameter]
public PteroConsole Console { get; set; }
[CascadingParameter]
public Server CurrentServer { get; set; }
[CascadingParameter]
public string[] Tags { get; set; }
private Dictionary<string, Type> Settings = new();
private Task Load(LazyLoader lazyLoader)
{
if(Tags.Contains("paperversion"))
Settings.Add("Paper version", typeof(PaperVersionSetting));
if(Tags.Contains("join2start"))
Settings.Add("Join2Start", typeof(Join2StartSetting));
if(Tags.Contains("javascriptversion"))
Settings.Add("Javascript version", typeof(JavascriptVersionSetting));
if(Tags.Contains("javascriptfile"))
Settings.Add("Javascript file", typeof(JavascriptFileSetting));
if(Tags.Contains("pythonversion"))
Settings.Add("Python version", typeof(PythonVersionSetting));
if(Tags.Contains("pythonfile"))
Settings.Add("Python file", typeof(PythonFileSetting));
Settings.Add("Server reset", typeof(ServerResetSetting));
return Task.CompletedTask;
}
private RenderFragment GetComponent(Type type) => builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}

View file

@ -0,0 +1,82 @@
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerRepository ServerRepository
@inject WingsApiHelper WingsApiHelper
@inject SmartTranslateService SmartTranslateService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label">
<TL>Javascript file</TL>
</label>
<input type="text" class="mb-2 form-control disabled" disabled="" value="@(PathAndFile)"/>
<button @onclick="Show" class="btn btn-primary"><TL>Change</TL></button>
</LazyLoader>
</div>
</div>
<FileSelectModal @ref="FileSelectModal"
Access="Access"
Filter="@(x => !x.IsFile || x.Name.EndsWith(".js"))"
Title="@(SmartTranslateService.Translate("Select javascript file to execute on start"))"
OnlyFolder="false"
OnCancel="() => { return Task.CompletedTask; }"
OnSubmit="OnSubmit">
</FileSelectModal>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
private string PathAndFile;
private FileAccess Access;
private FileSelectModal FileSelectModal;
private LazyLoader LazyLoader;
protected override void OnInitialized()
{
Access = new WingsFileAccess(WingsApiHelper,
null!,
CurrentServer,
null!,
null!
);
}
private async Task Load(LazyLoader lazyLoader)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_JS_FILE");
PathAndFile = v != null ? v.Value : "";
await InvokeAsync(StateHasChanged);
}
private async Task Show()
{
await FileSelectModal.Show();
}
private async Task OnSubmit(string path)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_JS_FILE");
if (v != null)
{
v.Value = path.TrimStart("/"[0]);
ServerRepository.Update(CurrentServer);
}
await LazyLoader.Reload();
}
}

View file

@ -8,29 +8,34 @@
@inject ServerRepository ServerRepository
@inject ImageRepository ImageRepository
@inject SmartTranslateService TranslationService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label"><TL>Javascript Version</TL></label>
<select class="mb-2 form-select" @bind="Image">
@foreach (var image in Images)
<label class="mb-2 form-label"><TL>Javascript version</TL></label>
<select @bind="ImageIndex" class="form-select mb-2">
@foreach (var image in DockerImages)
{
if (image == Image)
if (image.Id == SelectedImage.Id)
{
<option value="@(image)" selected="">@(image)</option>
<option value="@(image.Id)" selected="selected">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
else
{
<option value="@(image)">@(image)</option>
<option value="@(image.Id)">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
}
</select>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary">
</WButton>
</LazyLoader>
</div>
</div>
@ -40,44 +45,36 @@
[CascadingParameter]
public Server CurrentServer { get; set; }
private string[] Images;
private string Image;
private LazyLoader LazyLoader;
private List<DockerImage> DockerImages;
private DockerImage SelectedImage;
private async Task Load(LazyLoader lazyLoader)
private int ImageIndex
{
//TODO: Check if this is a redundant call
var serverImage = ImageRepository
get => SelectedImage.Id;
set { SelectedImage = DockerImages.First(x => x.Id == value); }
}
private Task Load(LazyLoader lazyLoader)
{
var image = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name);
var res = new List<string>();
foreach (var image in serverImage.DockerImages)
{
res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name));
}
Images = res.ToArray();
await InvokeAsync(StateHasChanged);
DockerImages = image.DockerImages;
SelectedImage = DockerImages[CurrentServer.DockerImageIndex];
return Task.CompletedTask;
}
private async Task Save()
{
var serverImage = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
var allImages = serverImage.DockerImages;
var imageToUse = allImages.First(x => x.Name.EndsWith(Image));
CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse);
ServerRepository.Update(CurrentServer);
CurrentServer.DockerImageIndex = DockerImages.IndexOf(SelectedImage);
ServerRepository.Update(CurrentServer);
await LazyLoader.Reload();
}
}

View file

@ -1,7 +1,5 @@
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Partials
@using Task = System.Threading.Tasks.Task
@using Logging.Net
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories

View file

@ -0,0 +1,82 @@
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerRepository ServerRepository
@inject WingsApiHelper WingsApiHelper
@inject SmartTranslateService SmartTranslateService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label">
<TL>Python file</TL>
</label>
<input type="text" class="mb-2 form-control disabled" disabled="" value="@(PathAndFile)"/>
<button @onclick="Show" class="btn btn-primary"><TL>Change</TL></button>
</LazyLoader>
</div>
</div>
<FileSelectModal @ref="FileSelectModal"
Access="Access"
Filter="@(x => !x.IsFile || x.Name.EndsWith(".py"))"
Title="@(SmartTranslateService.Translate("Select python file to execute on start"))"
OnlyFolder="false"
OnCancel="() => { return Task.CompletedTask; }"
OnSubmit="OnSubmit">
</FileSelectModal>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
private string PathAndFile;
private FileAccess Access;
private FileSelectModal FileSelectModal;
private LazyLoader LazyLoader;
protected override void OnInitialized()
{
Access = new WingsFileAccess(WingsApiHelper,
null!,
CurrentServer,
null!,
null!
);
}
private async Task Load(LazyLoader lazyLoader)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_PY_FILE");
PathAndFile = v != null ? v.Value : "";
await InvokeAsync(StateHasChanged);
}
private async Task Show()
{
await FileSelectModal.Show();
}
private async Task OnSubmit(string path)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_PY_FILE");
if (v != null)
{
v.Value = path.TrimStart("/"[0]);
ServerRepository.Update(CurrentServer);
}
await LazyLoader.Reload();
}
}

View file

@ -1,6 +1,4 @@
@using Moonlight.App.Services
@using Task = System.Threading.Tasks.Task
@using Moonlight.Shared.Components.Partials
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers
@ -15,24 +13,29 @@
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label"><TL>Python version</TL></label>
<select class="mb-2 form-select" @bind="Image">
@foreach (var image in Images)
<select @bind="ImageIndex" class="form-select mb-2">
@foreach (var image in DockerImages)
{
if (image == Image)
if (image.Id == SelectedImage.Id)
{
<option value="@(image)" selected="">@(image)</option>
<option value="@(image.Id)" selected="selected">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
else
{
<option value="@(image)">@(image)</option>
<option value="@(image.Id)">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
}
</select>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary">
</WButton>
</LazyLoader>
</div>
</div>
@ -42,43 +45,36 @@
[CascadingParameter]
public Server CurrentServer { get; set; }
private string[] Images;
private string Image;
private LazyLoader LazyLoader;
private List<DockerImage> DockerImages;
private DockerImage SelectedImage;
private async Task Load(LazyLoader lazyLoader)
private int ImageIndex
{
var serverImage = ImageRepository
get => SelectedImage.Id;
set { SelectedImage = DockerImages.First(x => x.Id == value); }
}
private Task Load(LazyLoader lazyLoader)
{
var image = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name);
var res = new List<string>();
foreach (var image in serverImage.DockerImages)
{
res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name));
}
Images = res.ToArray();
await InvokeAsync(StateHasChanged);
DockerImages = image.DockerImages;
SelectedImage = DockerImages[CurrentServer.DockerImageIndex];
return Task.CompletedTask;
}
private async Task Save()
{
var serverImage = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
var allImages = serverImage.DockerImages;
var imageToUse = allImages.First(x => x.Name.EndsWith(Image));
CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse);
ServerRepository.Update(CurrentServer);
CurrentServer.DockerImageIndex = DockerImages.IndexOf(SelectedImage);
ServerRepository.Update(CurrentServer);
await LazyLoader.Reload();
}
}

Some files were not shown because too many files have changed in this diff Show more