Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48ec2245c | ||
|
|
37a3a35120 | ||
|
|
c549d45d54 | ||
|
|
42a011541c | ||
|
|
71a8839f5f | ||
|
|
b99e8b5476 | ||
|
|
5793bd9747 | ||
|
|
24ce51a62e | ||
|
|
762d3cc1a1 | ||
|
|
ea19105006 | ||
|
|
a372c756e4 | ||
|
|
18b7c82613 | ||
|
|
5d91077d42 | ||
|
|
c0872d0d02 | ||
|
|
00722c4046 | ||
|
|
f16a29dafb | ||
|
|
6da8393612 | ||
|
|
2ef00b81b8 | ||
|
|
0be4ebc7ff | ||
|
|
e0d5e754b7 | ||
|
|
6972c2bb32 | ||
|
|
95ba81eab4 | ||
|
|
b9c094cafa | ||
|
|
bada1e829b | ||
|
|
1d33cf3110 | ||
|
|
abfcefba60 | ||
|
|
6efb443481 | ||
|
|
10c017932a | ||
|
|
a4be4bdc52 | ||
|
|
88d5a1de86 | ||
|
|
1b88827d16 | ||
|
|
12e25dc4bc | ||
|
|
37c3fc7a53 | ||
|
|
0c6cbe0ba7 | ||
|
|
5cf63240a3 | ||
|
|
4d04d12a39 | ||
|
|
b846b28802 | ||
|
|
b94217abc0 | ||
|
|
de0d6a3808 | ||
|
|
eca9b6ec52 | ||
|
|
b6e048e982 | ||
|
|
b8b2ae7865 | ||
|
|
0611988019 | ||
|
|
f9fd7199b6 | ||
|
|
6ff4861fc6 | ||
|
|
2db7748703 | ||
|
|
87744e4846 | ||
|
|
ce7125b50b | ||
|
|
31d8c3f469 | ||
|
|
c80622c2fd | ||
|
|
95e659e5f7 | ||
|
|
c155909e82 | ||
|
|
0011ed29b7 | ||
|
|
52c4ca0c0a | ||
|
|
c197d0ca96 | ||
|
|
25902034e9 | ||
|
|
b10db643fe | ||
|
|
4c7ffe6714 | ||
|
|
a0c2b45a61 | ||
|
|
e49a9d3505 | ||
|
|
7ddae9c3e1 | ||
|
|
290e865ae0 | ||
|
|
00ee625e2e | ||
|
|
274e2d93f2 | ||
|
|
781171f7c5 | ||
|
|
4a8618da79 | ||
|
|
296cf0db6f | ||
|
|
48cdba5155 | ||
|
|
35c1b255b5 | ||
|
|
244b920305 | ||
|
|
3d4a2128e2 | ||
|
|
1cf8430ad8 | ||
|
|
a9b7d10fb0 | ||
|
|
47e333630e | ||
|
|
8b41a9fc13 | ||
|
|
d09957fdac | ||
|
|
f9ecb61d71 | ||
|
|
ef92dd47ad | ||
|
|
c2c533675b | ||
|
|
569dd69bdd | ||
|
|
242870b3e1 | ||
|
|
30b6e45235 | ||
|
|
cd62fdc5f6 | ||
|
|
2c54b91e6c | ||
|
|
388deacf60 | ||
|
|
aa547038de | ||
|
|
78bfd68d63 | ||
|
|
17e3345b8a | ||
|
|
f95312c1e3 | ||
|
|
2144ca3823 | ||
|
|
de45ff40d8 | ||
|
|
606085c012 | ||
|
|
00525d8099 | ||
|
|
26617d67f5 | ||
|
|
600bec3417 | ||
|
|
4e85d1755a | ||
|
|
0832936933 | ||
|
|
ecda2ec6d1 | ||
|
|
6f3765a3bf | ||
|
|
29002d3445 | ||
|
|
6d0456a008 | ||
|
|
3e698123bb | ||
|
|
f3fb86819a | ||
|
|
e2248a8444 | ||
|
|
2cf2b77090 | ||
|
|
fedc9278d4 | ||
|
|
f29206a69b | ||
|
|
0658e55a78 | ||
|
|
21bea974a9 | ||
|
|
33ef09433e | ||
|
|
173bff67df | ||
|
|
512a989609 | ||
|
|
2d7dac5089 |
@@ -17,6 +17,15 @@ public class ConfigV1
|
|||||||
[Description("The url moonlight is accesible with from the internet")]
|
[Description("The url moonlight is accesible with from the internet")]
|
||||||
public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
|
public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
|
||||||
|
|
||||||
|
[JsonProperty("EnableLatencyCheck")]
|
||||||
|
[Description(
|
||||||
|
"This will enable a latency check for connections to moonlight. Users with an too high latency will be warned that moonlight might be buggy for them")]
|
||||||
|
public bool EnableLatencyCheck { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty("LatencyCheckThreshold")]
|
||||||
|
[Description("Specify the latency threshold which has to be reached in order to trigger the warning message")]
|
||||||
|
public int LatencyCheckThreshold { get; set; } = 1000;
|
||||||
|
|
||||||
[JsonProperty("Auth")] public AuthData Auth { get; set; } = new();
|
[JsonProperty("Auth")] public AuthData Auth { get; set; } = new();
|
||||||
|
|
||||||
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
||||||
@@ -50,6 +59,15 @@ public class ConfigV1
|
|||||||
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
|
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
|
||||||
|
|
||||||
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
|
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("Tickets")] public TicketsData Tickets { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TicketsData
|
||||||
|
{
|
||||||
|
[JsonProperty("WelcomeMessage")]
|
||||||
|
[Description("The message that will be sent when a user created a ticket")]
|
||||||
|
public string WelcomeMessage { get; set; } = "Welcome to the support";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StripeData
|
public class StripeData
|
||||||
@@ -272,11 +290,28 @@ public class ConfigV1
|
|||||||
public class SecurityData
|
public class SecurityData
|
||||||
{
|
{
|
||||||
[JsonProperty("Token")]
|
[JsonProperty("Token")]
|
||||||
[Description("This is the moonlight app token. It is used to encrypt and decrypt data and validte tokens and sessions")]
|
[Description("This is the moonlight app token. It is used to encrypt and decrypt data and validate tokens and sessions")]
|
||||||
[Blur]
|
[Blur]
|
||||||
public string Token { get; set; } = Guid.NewGuid().ToString();
|
public string Token { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
[JsonProperty("MalwareCheckOnStart")]
|
||||||
|
[Description(
|
||||||
|
"This option will enable the scanning for malware on every server before it has been started and if something has been found, the power action will be canceled")]
|
||||||
|
public bool MalwareCheckOnStart { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty("BlockIpDuration")]
|
||||||
|
[Description("The duration in minutes a ip will be blocked by the anti ddos system")]
|
||||||
|
public int BlockIpDuration { get; set; } = 15;
|
||||||
|
|
||||||
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
|
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("BlockDatacenterIps")]
|
||||||
|
[Description("If this option is enabled, users with an ip from datacenters will not be able to access the panel")]
|
||||||
|
public bool BlockDatacenterIps { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty("AllowCloudflareIps")]
|
||||||
|
[Description("Allow cloudflare ips to bypass the datacenter ip check")]
|
||||||
|
public bool AllowCloudflareIps { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ReCaptchaData
|
public class ReCaptchaData
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public class DataContext : DbContext
|
|||||||
public DbSet<IpBan> IpBans { get; set; }
|
public DbSet<IpBan> IpBans { get; set; }
|
||||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||||
public DbSet<SecurityLog> SecurityLogs { get; set; }
|
public DbSet<SecurityLog> SecurityLogs { get; set; }
|
||||||
|
public DbSet<BlocklistIp> BlocklistIps { get; set; }
|
||||||
|
public DbSet<WhitelistIp> WhitelistIps { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Ticket> Tickets { get; set; }
|
||||||
|
public DbSet<TicketMessage> TicketMessages { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
10
Moonlight/App/Database/Entities/BlocklistIp.cs
Normal file
10
Moonlight/App/Database/Entities/BlocklistIp.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class BlocklistIp
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Ip { get; set; } = "";
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public long Packets { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
19
Moonlight/App/Database/Entities/Ticket.cs
Normal file
19
Moonlight/App/Database/Entities/Ticket.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class Ticket
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string IssueTopic { get; set; } = "";
|
||||||
|
public string IssueDescription { get; set; } = "";
|
||||||
|
public string IssueTries { get; set; } = "";
|
||||||
|
public User CreatedBy { get; set; }
|
||||||
|
public User? AssignedTo { get; set; }
|
||||||
|
public TicketPriority Priority { get; set; }
|
||||||
|
public TicketStatus Status { get; set; }
|
||||||
|
public TicketSubject Subject { get; set; }
|
||||||
|
public int SubjectId { get; set; }
|
||||||
|
public List<TicketMessage> Messages { get; set; } = new();
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
14
Moonlight/App/Database/Entities/TicketMessage.cs
Normal file
14
Moonlight/App/Database/Entities/TicketMessage.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class TicketMessage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Content { get; set; } = "";
|
||||||
|
public string? AttachmentUrl { get; set; }
|
||||||
|
public User? Sender { get; set; }
|
||||||
|
public bool IsSystemMessage { get; set; }
|
||||||
|
public bool IsEdited { get; set; }
|
||||||
|
public bool IsSupportMessage { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
7
Moonlight/App/Database/Entities/WhitelistIp.cs
Normal file
7
Moonlight/App/Database/Entities/WhitelistIp.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class WhitelistIp
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Ip { get; set; } = "";
|
||||||
|
}
|
||||||
1107
Moonlight/App/Database/Migrations/20230721201950_AddIpRules.Designer.cs
generated
Normal file
1107
Moonlight/App/Database/Migrations/20230721201950_AddIpRules.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIpRules : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BlocklistIps",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Ip = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
Packets = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BlocklistIps", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WhitelistIps",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Ip = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WhitelistIps", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BlocklistIps");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "WhitelistIps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1233
Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs
generated
Normal file
1233
Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddNewTicketModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tickets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
IssueTopic = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IssueDescription = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IssueTries = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedById = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AssignedToId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Priority = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Subject = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SubjectId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tickets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Users_AssignedToId",
|
||||||
|
column: x => x.AssignedToId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Users_CreatedById",
|
||||||
|
column: x => x.CreatedById,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TicketMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Content = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
AttachmentUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
SenderId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
IsSystemMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsEdited = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsSupportMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
TicketId = table.Column<int>(type: "int", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TicketMessages", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TicketMessages_Tickets_TicketId",
|
||||||
|
column: x => x.TicketId,
|
||||||
|
principalTable: "Tickets",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TicketMessages_Users_SenderId",
|
||||||
|
column: x => x.SenderId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TicketMessages_SenderId",
|
||||||
|
table: "TicketMessages",
|
||||||
|
column: "SenderId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TicketMessages_TicketId",
|
||||||
|
table: "TicketMessages",
|
||||||
|
column: "TicketId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tickets_AssignedToId",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "AssignedToId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tickets_CreatedById",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "CreatedById");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TicketMessages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tickets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,30 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "7.0.3")
|
.HasAnnotation("ProductVersion", "7.0.3")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.BlocklistIp", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Ip")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<long>("Packets")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("BlocklistIps");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -690,6 +714,97 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("SupportChatMessages");
|
b.ToTable("SupportChatMessages");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("AssignedToId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("CreatedById")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("IssueDescription")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("IssueTopic")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("IssueTries")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Subject")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SubjectId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssignedToId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedById");
|
||||||
|
|
||||||
|
b.ToTable("Tickets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentUrl")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEdited")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSupportMessage")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystemMessage")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int?>("SenderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("TicketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId");
|
||||||
|
|
||||||
|
b.HasIndex("TicketId");
|
||||||
|
|
||||||
|
b.ToTable("TicketMessages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -842,6 +957,21 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("WebSpaces");
|
b.ToTable("WebSpaces");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.WhitelistIp", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Ip")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("WhitelistIps");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
||||||
@@ -1000,6 +1130,36 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.Navigation("Sender");
|
b.Navigation("Sender");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "AssignedTo")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssignedToId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "CreatedBy")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatedById")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssignedTo");
|
||||||
|
|
||||||
|
b.Navigation("CreatedBy");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Ticket", null)
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("TicketId");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
|
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
|
||||||
@@ -1055,6 +1215,11 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.Navigation("Variables");
|
b.Navigation("Variables");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Databases");
|
b.Navigation("Databases");
|
||||||
|
|||||||
119
Moonlight/App/Helpers/BackupHelper.cs
Normal file
119
Moonlight/App/Helpers/BackupHelper.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class BackupHelper
|
||||||
|
{
|
||||||
|
public async Task CreateBackup(string path)
|
||||||
|
{
|
||||||
|
Logger.Info("Started moonlight backup creation");
|
||||||
|
Logger.Info($"This backup will be saved to '{path}'");
|
||||||
|
|
||||||
|
var stopWatch = new Stopwatch();
|
||||||
|
stopWatch.Start();
|
||||||
|
|
||||||
|
var cachePath = PathBuilder.Dir("storage", "backups", "cache");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(cachePath);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Exporting database
|
||||||
|
//
|
||||||
|
|
||||||
|
Logger.Info("Exporting database");
|
||||||
|
|
||||||
|
var configService = new ConfigService(new());
|
||||||
|
var dataContext = new DataContext(configService);
|
||||||
|
|
||||||
|
await using MySqlConnection conn = new MySqlConnection(dataContext.Database.GetConnectionString());
|
||||||
|
await using MySqlCommand cmd = new MySqlCommand();
|
||||||
|
using MySqlBackup mb = new MySqlBackup(cmd);
|
||||||
|
|
||||||
|
cmd.Connection = conn;
|
||||||
|
await conn.OpenAsync();
|
||||||
|
mb.ExportToFile(PathBuilder.File(cachePath, "database.sql"));
|
||||||
|
await conn.CloseAsync();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Saving config
|
||||||
|
//
|
||||||
|
|
||||||
|
Logger.Info("Saving configuration");
|
||||||
|
File.Copy(
|
||||||
|
PathBuilder.File("storage", "configs", "config.json"),
|
||||||
|
PathBuilder.File(cachePath, "config.json"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Saving all storage items needed to restore the panel
|
||||||
|
//
|
||||||
|
|
||||||
|
Logger.Info("Saving resources");
|
||||||
|
CopyDirectory(
|
||||||
|
PathBuilder.Dir("storage", "resources"),
|
||||||
|
PathBuilder.Dir(cachePath, "resources"));
|
||||||
|
|
||||||
|
Logger.Info("Saving logs");
|
||||||
|
CopyDirectory(
|
||||||
|
PathBuilder.Dir("storage", "logs"),
|
||||||
|
PathBuilder.Dir(cachePath, "logs"));
|
||||||
|
|
||||||
|
Logger.Info("Saving uploads");
|
||||||
|
CopyDirectory(
|
||||||
|
PathBuilder.Dir("storage", "uploads"),
|
||||||
|
PathBuilder.Dir(cachePath, "uploads"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Compressing the backup to a single file
|
||||||
|
//
|
||||||
|
|
||||||
|
Logger.Info("Compressing");
|
||||||
|
ZipFile.CreateFromDirectory(cachePath,
|
||||||
|
path,
|
||||||
|
CompressionLevel.Fastest,
|
||||||
|
false);
|
||||||
|
|
||||||
|
Directory.Delete(cachePath, true);
|
||||||
|
|
||||||
|
stopWatch.Stop();
|
||||||
|
Logger.Info($"Backup successfully created. Took {stopWatch.Elapsed.TotalSeconds} seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyDirectory(string sourceDirName, string destDirName, bool copySubDirs = true)
|
||||||
|
{
|
||||||
|
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
|
||||||
|
|
||||||
|
if (!dir.Exists)
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(destDirName))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo[] files = dir.GetFiles();
|
||||||
|
|
||||||
|
foreach (FileInfo file in files)
|
||||||
|
{
|
||||||
|
string tempPath = Path.Combine(destDirName, file.Name);
|
||||||
|
file.CopyTo(tempPath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copySubDirs)
|
||||||
|
{
|
||||||
|
DirectoryInfo[] dirs = dir.GetDirectories();
|
||||||
|
|
||||||
|
foreach (DirectoryInfo subdir in dirs)
|
||||||
|
{
|
||||||
|
string tempPath = Path.Combine(destDirName, subdir.Name);
|
||||||
|
CopyDirectory(subdir.FullName, tempPath, copySubDirs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Moonlight/App/Helpers/ByteSizeValue.cs
Normal file
56
Moonlight/App/Helpers/ByteSizeValue.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class ByteSizeValue
|
||||||
|
{
|
||||||
|
public long Bytes { get; set; }
|
||||||
|
|
||||||
|
public long KiloBytes
|
||||||
|
{
|
||||||
|
get => Bytes / 1024;
|
||||||
|
set => Bytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long MegaBytes
|
||||||
|
{
|
||||||
|
get => KiloBytes / 1024;
|
||||||
|
set => KiloBytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GigaBytes
|
||||||
|
{
|
||||||
|
get => MegaBytes / 1024;
|
||||||
|
set => MegaBytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromBytes(long bytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Bytes = bytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromKiloBytes(long kiloBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
KiloBytes = kiloBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromMegaBytes(long megaBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
MegaBytes = megaBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromGigaBytes(long gigaBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
GigaBytes = gigaBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class ComponentHelper
|
||||||
|
{
|
||||||
|
public static RenderFragment FromType(Type type) => builder =>
|
||||||
|
{
|
||||||
|
builder.OpenComponent(0, type);
|
||||||
|
builder.CloseComponent();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -45,7 +45,18 @@ public class DatabaseCheckupService
|
|||||||
{
|
{
|
||||||
Logger.Info($"{migrations.Length} migrations pending. Updating now");
|
Logger.Info($"{migrations.Length} migrations pending. Updating now");
|
||||||
|
|
||||||
await BackupDatabase();
|
try
|
||||||
|
{
|
||||||
|
var backupHelper = new BackupHelper();
|
||||||
|
await backupHelper.CreateBackup(
|
||||||
|
PathBuilder.File("storage", "backups", $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip"));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Fatal("Unable to create backup");
|
||||||
|
Logger.Fatal(e);
|
||||||
|
Logger.Fatal("Moonlight will continue to start and apply the migrations without a backup");
|
||||||
|
}
|
||||||
|
|
||||||
Logger.Info("Applying migrations");
|
Logger.Info("Applying migrations");
|
||||||
|
|
||||||
@@ -58,53 +69,4 @@ public class DatabaseCheckupService
|
|||||||
Logger.Info("Database is up-to-date. No migrations have been performed");
|
Logger.Info("Database is up-to-date. No migrations have been performed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BackupDatabase()
|
|
||||||
{
|
|
||||||
Logger.Info("Creating backup from database");
|
|
||||||
|
|
||||||
var configService = new ConfigService(new StorageService());
|
|
||||||
var dateTimeService = new DateTimeService();
|
|
||||||
|
|
||||||
var config = configService.Get().Moonlight.Database;
|
|
||||||
|
|
||||||
var connectionString = $"host={config.Host};" +
|
|
||||||
$"port={config.Port};" +
|
|
||||||
$"database={config.Database};" +
|
|
||||||
$"uid={config.Username};" +
|
|
||||||
$"pwd={config.Password}";
|
|
||||||
|
|
||||||
string file = PathBuilder.File("storage", "backups", $"{dateTimeService.GetCurrentUnix()}-mysql.sql");
|
|
||||||
|
|
||||||
Logger.Info($"Saving it to: {file}");
|
|
||||||
Logger.Info("Starting backup...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
|
|
||||||
await using MySqlConnection conn = new MySqlConnection(connectionString);
|
|
||||||
await using MySqlCommand cmd = new MySqlCommand();
|
|
||||||
using MySqlBackup mb = new MySqlBackup(cmd);
|
|
||||||
|
|
||||||
cmd.Connection = conn;
|
|
||||||
await conn.OpenAsync();
|
|
||||||
mb.ExportToFile(file);
|
|
||||||
await conn.CloseAsync();
|
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Fatal("-----------------------------------------------");
|
|
||||||
Logger.Fatal("Unable to create backup for moonlight database");
|
|
||||||
Logger.Fatal("Moonlight will start anyways in 30 seconds");
|
|
||||||
Logger.Fatal("-----------------------------------------------");
|
|
||||||
Logger.Fatal(e);
|
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(30));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -43,19 +43,22 @@ public class WingsFileAccess : FileAccess
|
|||||||
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
|
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var x = new List<FileData>();
|
var result = new List<FileData>();
|
||||||
|
|
||||||
foreach (var response in res)
|
foreach (var resGrouped in res.GroupBy(x => x.Directory))
|
||||||
{
|
{
|
||||||
x.Add(new()
|
foreach (var resItem in resGrouped.OrderBy(x => x.Name))
|
||||||
{
|
{
|
||||||
Name = response.Name,
|
result.Add(new()
|
||||||
Size = response.File ? response.Size : 0,
|
{
|
||||||
IsFile = response.File,
|
Name = resItem.Name,
|
||||||
|
Size = resItem.File ? resItem.Size : 0,
|
||||||
|
IsFile = resItem.File,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return x.ToArray();
|
return result.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task Cd(string dir)
|
public override Task Cd(string dir)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
using Moonlight.App.Services;
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
namespace Moonlight.App.Helpers;
|
namespace Moonlight.App.Helpers;
|
||||||
@@ -156,11 +157,47 @@ public static class Formatter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double BytesToGb(long bytes)
|
public static double CalculateAverage(List<double> values)
|
||||||
{
|
{
|
||||||
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
|
if (values == null || values.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("The list cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
double gigabytes = (double)bytes / gbMultiplier;
|
double sum = 0;
|
||||||
return gigabytes;
|
foreach (double value in values)
|
||||||
|
{
|
||||||
|
sum += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / values.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculatePercentage(double part, double total)
|
||||||
|
{
|
||||||
|
if (total == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (part / total) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RenderFragment FormatLineBreaks(string content)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
foreach (var line in arr)
|
||||||
|
{
|
||||||
|
builder.AddContent(i, line);
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
builder.AddMarkupContent(i, "<br/>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,4 +43,15 @@ public static class StringHelper
|
|||||||
|
|
||||||
return firstChar + restOfString;
|
return firstChar + restOfString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string CutInHalf(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return input;
|
||||||
|
|
||||||
|
int length = input.Length;
|
||||||
|
int halfLength = length / 2;
|
||||||
|
|
||||||
|
return input.Substring(0, halfLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +218,16 @@ public class WingsConsole : IDisposable
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "install output":
|
case "install output":
|
||||||
|
if (ServerState != ServerState.Installing)
|
||||||
|
{
|
||||||
|
// Because wings is sending "install output" events BEFORE
|
||||||
|
// sending the "install started" event,
|
||||||
|
// we need to set the install state here
|
||||||
|
// See https://github.com/pterodactyl/panel/issues/4853
|
||||||
|
// for more details
|
||||||
|
await UpdateServerState(ServerState.Installing);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var line in eventData.Args)
|
foreach (var line in eventData.Args)
|
||||||
{
|
{
|
||||||
await SaveMessage(line);
|
await SaveMessage(line);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities;
|
|||||||
using Moonlight.App.Events;
|
using Moonlight.App.Events;
|
||||||
using Moonlight.App.Http.Requests.Daemon;
|
using Moonlight.App.Http.Requests.Daemon;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
|
using Moonlight.App.Services.Background;
|
||||||
|
|
||||||
namespace Moonlight.App.Http.Controllers.Api.Remote;
|
namespace Moonlight.App.Http.Controllers.Api.Remote;
|
||||||
|
|
||||||
@@ -10,19 +11,17 @@ namespace Moonlight.App.Http.Controllers.Api.Remote;
|
|||||||
[Route("api/remote/ddos")]
|
[Route("api/remote/ddos")]
|
||||||
public class DdosController : Controller
|
public class DdosController : Controller
|
||||||
{
|
{
|
||||||
private readonly NodeRepository NodeRepository;
|
private readonly Repository<Node> NodeRepository;
|
||||||
private readonly EventSystem Event;
|
private readonly DdosProtectionService DdosProtectionService;
|
||||||
private readonly DdosAttackRepository DdosAttackRepository;
|
|
||||||
|
|
||||||
public DdosController(NodeRepository nodeRepository, EventSystem eventSystem, DdosAttackRepository ddosAttackRepository)
|
public DdosController(Repository<Node> nodeRepository, DdosProtectionService ddosProtectionService)
|
||||||
{
|
{
|
||||||
NodeRepository = nodeRepository;
|
NodeRepository = nodeRepository;
|
||||||
Event = eventSystem;
|
DdosProtectionService = ddosProtectionService;
|
||||||
DdosAttackRepository = ddosAttackRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("update")]
|
[HttpPost("start")]
|
||||||
public async Task<ActionResult> Update([FromBody] DdosStatus ddosStatus)
|
public async Task<ActionResult> Start([FromBody] DdosStart ddosStart)
|
||||||
{
|
{
|
||||||
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
var id = tokenData.Split(".")[0];
|
var id = tokenData.Split(".")[0];
|
||||||
@@ -36,17 +35,25 @@ public class DdosController : Controller
|
|||||||
if (token != node.Token)
|
if (token != node.Token)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var ddosAttack = new DdosAttack()
|
await DdosProtectionService.ProcessDdosSignal(ddosStart.Ip, ddosStart.Packets);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("stop")]
|
||||||
|
public async Task<ActionResult> Stop([FromBody] DdosStop ddosStop)
|
||||||
{
|
{
|
||||||
Ongoing = ddosStatus.Ongoing,
|
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
Data = ddosStatus.Data,
|
var id = tokenData.Split(".")[0];
|
||||||
Ip = ddosStatus.Ip,
|
var token = tokenData.Split(".")[1];
|
||||||
Node = node
|
|
||||||
};
|
|
||||||
|
|
||||||
ddosAttack = DdosAttackRepository.Add(ddosAttack);
|
var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
|
||||||
|
|
||||||
await Event.Emit("node.ddos", ddosAttack);
|
if (node == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
if (token != node.Token)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|||||||
7
Moonlight/App/Http/Requests/Daemon/DdosStart.cs
Normal file
7
Moonlight/App/Http/Requests/Daemon/DdosStart.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Http.Requests.Daemon;
|
||||||
|
|
||||||
|
public class DdosStart
|
||||||
|
{
|
||||||
|
public string Ip { get; set; } = "";
|
||||||
|
public long Packets { get; set; }
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Moonlight.App.Http.Requests.Daemon;
|
|
||||||
|
|
||||||
public class DdosStatus
|
|
||||||
{
|
|
||||||
public bool Ongoing { get; set; }
|
|
||||||
public long Data { get; set; }
|
|
||||||
public string Ip { get; set; } = "";
|
|
||||||
}
|
|
||||||
7
Moonlight/App/Http/Requests/Daemon/DdosStop.cs
Normal file
7
Moonlight/App/Http/Requests/Daemon/DdosStop.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Http.Requests.Daemon;
|
||||||
|
|
||||||
|
public class DdosStop
|
||||||
|
{
|
||||||
|
public string Ip { get; set; } = "";
|
||||||
|
public long Traffic { get; set; }
|
||||||
|
}
|
||||||
54
Moonlight/App/MalwareScans/DiscordNukeScan.cs
Normal file
54
Moonlight/App/MalwareScans/DiscordNukeScan.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class DiscordNukeScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Discord nuke";
|
||||||
|
public override string Description => "Discord nuke bot detector";
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
|
||||||
|
var files = await access.Ls();
|
||||||
|
var filteredFiles = files.Where(x =>
|
||||||
|
x.Name.EndsWith(".py") ||
|
||||||
|
x.Name.EndsWith(".js") ||
|
||||||
|
x.Name.EndsWith(".json") ||
|
||||||
|
x.Name.EndsWith(".env"));
|
||||||
|
|
||||||
|
foreach (var file in filteredFiles)
|
||||||
|
{
|
||||||
|
var content = await access.Read(file);
|
||||||
|
var filteredContent = content.ToLower();
|
||||||
|
|
||||||
|
if (filteredContent.Contains("quake") ||
|
||||||
|
filteredContent.Contains("nuked by") ||
|
||||||
|
filteredContent.Contains("nuke bot") ||
|
||||||
|
(filteredContent.Contains("fucked by") && filteredContent.Contains("nuke"))) // fucked by in context with nuke
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = "Discord nuke bot",
|
||||||
|
Description = "Found suspicious content which may indicate there is a nuke bot running",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.Any(x => x.Name == "nukes.json"))
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = "Discord nuke bot",
|
||||||
|
Description = "Found suspicious content which may indicate there is a nuke bot running",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Moonlight/App/MalwareScans/FakePlayerPluginScan.cs
Normal file
39
Moonlight/App/MalwareScans/FakePlayerPluginScan.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class FakePlayerPluginScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Fake player plugin scan";
|
||||||
|
public override string Description => "This scan is a simple fake player plugin scan provided by moonlight";
|
||||||
|
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
var fileElements = await access.Ls();
|
||||||
|
|
||||||
|
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
|
||||||
|
{
|
||||||
|
await access.Cd("plugins");
|
||||||
|
fileElements = await access.Ls();
|
||||||
|
|
||||||
|
foreach (var fileElement in fileElements)
|
||||||
|
{
|
||||||
|
if (fileElement.Name.ToLower().Contains("fakeplayer"))
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = "Fake player plugin",
|
||||||
|
Description = $"Suspicious plugin file: {fileElement.Name}",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Moonlight/App/MalwareScans/MinerJarScan.cs
Normal file
37
Moonlight/App/MalwareScans/MinerJarScan.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class MinerJarScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Miner jar scan";
|
||||||
|
public override string Description => "This scan is a simple miner jar scan provided by moonlight";
|
||||||
|
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
var fileElements = await access.Ls();
|
||||||
|
|
||||||
|
if (fileElements.Any(x => x.Name == "libraries" && !x.IsFile))
|
||||||
|
{
|
||||||
|
await access.Cd("libraries");
|
||||||
|
|
||||||
|
fileElements = await access.Ls();
|
||||||
|
|
||||||
|
if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile))
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = "Found Miner",
|
||||||
|
Description = "Detected suspicious library directory which may contain a script for miners",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Moonlight/App/MalwareScans/MinerScan.cs
Normal file
35
Moonlight/App/MalwareScans/MinerScan.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class MinerScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Miner (NEZHA)";
|
||||||
|
public override string Description => "Probably a miner";
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
var files = await access.Ls();
|
||||||
|
|
||||||
|
foreach (var file in files.Where(x => x.IsFile && (x.Name.EndsWith(".sh") || x.Name.EndsWith(".yml")) || x.Name == "bed"))
|
||||||
|
{
|
||||||
|
var content = await access.Read(file);
|
||||||
|
|
||||||
|
if (content.ToLower().Contains("nezha"))
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = "Miner",
|
||||||
|
Description = "Miner start script (NEZHA)",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Moonlight/App/MalwareScans/ProxyScan.cs
Normal file
36
Moonlight/App/MalwareScans/ProxyScan.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class ProxyScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Proxy software";
|
||||||
|
public override string Description => "Software to use nodes as a proxy";
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
|
||||||
|
var files = await access.Ls();
|
||||||
|
|
||||||
|
foreach (var file in files.Where(x => x.Name.EndsWith(".sh")))
|
||||||
|
{
|
||||||
|
var fileContent = await access.Read(file);
|
||||||
|
var processableContent = fileContent.ToLower();
|
||||||
|
|
||||||
|
if (processableContent.Contains("t-e-s-tweb"))
|
||||||
|
{
|
||||||
|
return new MalwareScanResult()
|
||||||
|
{
|
||||||
|
Title = "Proxy software",
|
||||||
|
Description = "Software to use nodes as a proxy",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Moonlight/App/MalwareScans/SelfBotCodeScan.cs
Normal file
35
Moonlight/App/MalwareScans/SelfBotCodeScan.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class SelfBotCodeScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Selfbot code scan";
|
||||||
|
public override string Description => "This scan is a simple selfbot code scan provided by moonlight";
|
||||||
|
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
var fileElements = await access.Ls();
|
||||||
|
|
||||||
|
foreach (var script in fileElements.Where(x => x.Name.EndsWith(".py") && x.IsFile))
|
||||||
|
{
|
||||||
|
var rawScript = await access.Read(script);
|
||||||
|
|
||||||
|
if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking
|
||||||
|
{
|
||||||
|
return new MalwareScanResult
|
||||||
|
{
|
||||||
|
Title = "Potential selfbot",
|
||||||
|
Description = $"Suspicious script file: {script.Name}",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Moonlight/App/MalwareScans/SelfBotScan.cs
Normal file
30
Moonlight/App/MalwareScans/SelfBotScan.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.MalwareScans;
|
||||||
|
|
||||||
|
public class SelfBotScan : MalwareScan
|
||||||
|
{
|
||||||
|
public override string Name => "Selfbot Scan";
|
||||||
|
public override string Description => "This scan is a simple selfbot scan provided by moonlight";
|
||||||
|
|
||||||
|
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var serverService = serviceProvider.GetRequiredService<ServerService>();
|
||||||
|
var access = await serverService.CreateFileAccess(server, null!);
|
||||||
|
var fileElements = await access.Ls();
|
||||||
|
|
||||||
|
if (fileElements.Any(x => x.Name == "tokens.txt"))
|
||||||
|
{
|
||||||
|
return new MalwareScanResult
|
||||||
|
{
|
||||||
|
Title = "Found SelfBot",
|
||||||
|
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
|
||||||
|
Author = "Marcel Baumgartner"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Moonlight/App/Models/Forms/CreateTicketDataModel.cs
Normal file
21
Moonlight/App/Models/Forms/CreateTicketDataModel.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms;
|
||||||
|
|
||||||
|
public class CreateTicketDataModel
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a issue topic")]
|
||||||
|
[MinLength(5, ErrorMessage = "The issue topic needs to be longer than 5 characters")]
|
||||||
|
public string IssueTopic { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a issue description")]
|
||||||
|
[MinLength(10, ErrorMessage = "The issue description needs to be longer than 10 characters")]
|
||||||
|
public string IssueDescription { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify your tries to solve this issue")]
|
||||||
|
public string IssueTries { get; set; }
|
||||||
|
|
||||||
|
public TicketSubject Subject { get; set; }
|
||||||
|
public int SubjectId { get; set; }
|
||||||
|
}
|
||||||
10
Moonlight/App/Models/Misc/MalwareScan.cs
Normal file
10
Moonlight/App/Models/Misc/MalwareScan.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public abstract class MalwareScan
|
||||||
|
{
|
||||||
|
public abstract string Name { get; }
|
||||||
|
public abstract string Description { get; }
|
||||||
|
public abstract Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
public class MalwareScanResult
|
public class MalwareScanResult
|
||||||
{
|
{
|
||||||
public string Title { get; set; } = "";
|
public string Title { get; set; }
|
||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
public string Author { get; set; } = "";
|
public string Author { get; set; } = "";
|
||||||
}
|
}
|
||||||
6
Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs
Normal file
6
Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public class OfficialMoonlightPlugin
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
9
Moonlight/App/Models/Misc/TicketPriority.cs
Normal file
9
Moonlight/App/Models/Misc/TicketPriority.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketPriority
|
||||||
|
{
|
||||||
|
Low = 0,
|
||||||
|
Medium = 1,
|
||||||
|
High = 2,
|
||||||
|
Critical = 3
|
||||||
|
}
|
||||||
9
Moonlight/App/Models/Misc/TicketStatus.cs
Normal file
9
Moonlight/App/Models/Misc/TicketStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketStatus
|
||||||
|
{
|
||||||
|
Closed = 0,
|
||||||
|
Open = 1,
|
||||||
|
WaitingForUser = 2,
|
||||||
|
Pending = 3
|
||||||
|
}
|
||||||
9
Moonlight/App/Models/Misc/TicketSubject.cs
Normal file
9
Moonlight/App/Models/Misc/TicketSubject.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketSubject
|
||||||
|
{
|
||||||
|
Webspace = 0,
|
||||||
|
Server = 1,
|
||||||
|
Domain = 2,
|
||||||
|
Other = 3
|
||||||
|
}
|
||||||
@@ -16,6 +16,13 @@ public static class Permissions
|
|||||||
Description = "View statistical information about the moonlight instance"
|
Description = "View statistical information about the moonlight instance"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static Permission AdminSysPlugins = new()
|
||||||
|
{
|
||||||
|
Index = 2,
|
||||||
|
Name = "Admin system plugins",
|
||||||
|
Description = "View and install plugins"
|
||||||
|
};
|
||||||
|
|
||||||
public static Permission AdminDomains = new()
|
public static Permission AdminDomains = new()
|
||||||
{
|
{
|
||||||
Index = 4,
|
Index = 4,
|
||||||
@@ -44,13 +51,6 @@ public static class Permissions
|
|||||||
Description = "Create a new shared domain in the admin area"
|
Description = "Create a new shared domain in the admin area"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Permission AdminNodeDdos = new()
|
|
||||||
{
|
|
||||||
Index = 8,
|
|
||||||
Name = "Admin Node DDoS",
|
|
||||||
Description = "Manage DDoS protection for nodes in the admin area"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static Permission AdminNodeEdit = new()
|
public static Permission AdminNodeEdit = new()
|
||||||
{
|
{
|
||||||
Index = 9,
|
Index = 9,
|
||||||
@@ -401,6 +401,27 @@ public static class Permissions
|
|||||||
Description = "View the security logs"
|
Description = "View the security logs"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static Permission AdminSecurityDdos = new()
|
||||||
|
{
|
||||||
|
Index = 59,
|
||||||
|
Name = "Admin security ddos",
|
||||||
|
Description = "Manage the integrated ddos protection"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Permission AdminChangelog = new()
|
||||||
|
{
|
||||||
|
Index = 60,
|
||||||
|
Name = "Admin changelog",
|
||||||
|
Description = "View the changelog"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Permission AdminStatisticsLive = new()
|
||||||
|
{
|
||||||
|
Index = 61,
|
||||||
|
Name = "Admin statistics live",
|
||||||
|
Description = "View the live statistics"
|
||||||
|
};
|
||||||
|
|
||||||
public static Permission? FromString(string name)
|
public static Permission? FromString(string name)
|
||||||
{
|
{
|
||||||
var type = typeof(Permissions);
|
var type = typeof(Permissions);
|
||||||
|
|||||||
17
Moonlight/App/Plugin/MoonlightPlugin.cs
Normal file
17
Moonlight/App/Plugin/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Plugin.UI.Servers;
|
||||||
|
using Moonlight.App.Plugin.UI.Webspaces;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin;
|
||||||
|
|
||||||
|
public abstract class MoonlightPlugin
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "N/A";
|
||||||
|
public string Author { get; set; } = "N/A";
|
||||||
|
public string Version { get; set; } = "N/A";
|
||||||
|
|
||||||
|
public Func<ServerPageContext, Task>? OnBuildServerPage { get; set; }
|
||||||
|
public Func<WebspacePageContext, Task>? OnBuildWebspacePage { get; set; }
|
||||||
|
public Func<IServiceCollection, Task>? OnBuildServices { get; set; }
|
||||||
|
public Func<List<MalwareScan>, Task<List<MalwareScan>>>? OnBuildMalwareScans { get; set; }
|
||||||
|
}
|
||||||
12
Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs
Normal file
12
Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin.UI.Servers;
|
||||||
|
|
||||||
|
public class ServerPageContext
|
||||||
|
{
|
||||||
|
public List<ServerTab> Tabs { get; set; } = new();
|
||||||
|
public List<ServerSetting> Settings { get; set; } = new();
|
||||||
|
public Server Server { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public string[] ImageTags { get; set; }
|
||||||
|
}
|
||||||
9
Moonlight/App/Plugin/UI/Servers/ServerSetting.cs
Normal file
9
Moonlight/App/Plugin/UI/Servers/ServerSetting.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin.UI.Servers;
|
||||||
|
|
||||||
|
public class ServerSetting
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public RenderFragment Component { get; set; }
|
||||||
|
}
|
||||||
11
Moonlight/App/Plugin/UI/Servers/ServerTab.cs
Normal file
11
Moonlight/App/Plugin/UI/Servers/ServerTab.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin.UI.Servers;
|
||||||
|
|
||||||
|
public class ServerTab
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Route { get; set; }
|
||||||
|
public string Icon { get; set; }
|
||||||
|
public RenderFragment Component { get; set; }
|
||||||
|
}
|
||||||
10
Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs
Normal file
10
Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin.UI.Webspaces;
|
||||||
|
|
||||||
|
public class WebspacePageContext
|
||||||
|
{
|
||||||
|
public List<WebspaceTab> Tabs { get; set; } = new();
|
||||||
|
public User User { get; set; }
|
||||||
|
public WebSpace WebSpace { get; set; }
|
||||||
|
}
|
||||||
10
Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs
Normal file
10
Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugin.UI.Webspaces;
|
||||||
|
|
||||||
|
public class WebspaceTab
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "N/A";
|
||||||
|
public string Route { get; set; } = "/";
|
||||||
|
public RenderFragment Component { get; set; }
|
||||||
|
}
|
||||||
@@ -93,14 +93,14 @@ public class CleanupService
|
|||||||
Logger.Debug($"Max CPU: {maxCpu}");
|
Logger.Debug($"Max CPU: {maxCpu}");
|
||||||
executeCleanup = true;
|
executeCleanup = true;
|
||||||
}
|
}
|
||||||
else if (Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used) <
|
else if (ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes - ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes <
|
||||||
minMemory / 1024D)
|
minMemory)
|
||||||
{
|
{
|
||||||
Logger.Debug($"{node.Name}: Memory Usage is too high");
|
Logger.Debug($"{node.Name}: Memory Usage is too high");
|
||||||
Logger.Debug($"Memory (Total): {Formatter.BytesToGb(memoryMetrics.Total)}");
|
Logger.Debug($"Memory (Total): {ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes}");
|
||||||
Logger.Debug($"Memory (Used): {Formatter.BytesToGb(memoryMetrics.Used)}");
|
Logger.Debug($"Memory (Used): {ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes}");
|
||||||
Logger.Debug(
|
Logger.Debug(
|
||||||
$"Memory (Free): {Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used)}");
|
$"Memory (Free): {ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes - ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes}");
|
||||||
Logger.Debug($"Min Memory: {minMemory}");
|
Logger.Debug($"Min Memory: {minMemory}");
|
||||||
executeCleanup = true;
|
executeCleanup = true;
|
||||||
}
|
}
|
||||||
|
|||||||
129
Moonlight/App/Services/Background/DdosProtectionService.cs
Normal file
129
Moonlight/App/Services/Background/DdosProtectionService.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using Moonlight.App.ApiClients.Daemon;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Background;
|
||||||
|
|
||||||
|
public class DdosProtectionService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
|
public DdosProtectionService(IServiceScopeFactory serviceScopeFactory)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
|
||||||
|
Task.Run(UnBlocker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnBlocker()
|
||||||
|
{
|
||||||
|
var periodicTimer = new PeriodicTimer(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||||
|
|
||||||
|
var ips = blocklistIpRepo
|
||||||
|
.Get()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var ip in ips)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow > ip.ExpiresAt)
|
||||||
|
{
|
||||||
|
blocklistIpRepo.Delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCount = blocklistIpRepo
|
||||||
|
.Get()
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
if (newCount != ips.Length)
|
||||||
|
{
|
||||||
|
await RebuildNodeFirewalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
await periodicTimer.WaitForNextTickAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RebuildNodeFirewalls()
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||||
|
var nodeRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
|
||||||
|
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
|
||||||
|
|
||||||
|
var ips = blocklistIpRepo
|
||||||
|
.Get()
|
||||||
|
.Select(x => x.Ip)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var node in nodeRepo.Get().ToArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await nodeService.RebuildFirewall(node, ips);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warn($"Error rebuilding firewall on node {node.Name}");
|
||||||
|
Logger.Warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ProcessDdosSignal(string ip, long packets)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||||
|
var whitelistRepo = scope.ServiceProvider.GetRequiredService<Repository<WhitelistIp>>();
|
||||||
|
|
||||||
|
var whitelistIps = whitelistRepo.Get().ToArray();
|
||||||
|
|
||||||
|
if(whitelistIps.Any(x => x.Ip == ip))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var blocklistIps = blocklistRepo.Get().ToArray();
|
||||||
|
|
||||||
|
if(blocklistIps.Any(x => x.Ip == ip))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await BlocklistIp(ip, packets);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BlocklistIp(string ip, long packets)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||||
|
var configService = scope.ServiceProvider.GetRequiredService<ConfigService>();
|
||||||
|
var eventSystem = scope.ServiceProvider.GetRequiredService<EventSystem>();
|
||||||
|
|
||||||
|
var blocklistIp = blocklistRepo.Add(new()
|
||||||
|
{
|
||||||
|
Ip = ip,
|
||||||
|
Packets = packets,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddMinutes(configService.Get().Moonlight.Security.BlockIpDuration),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
await RebuildNodeFirewalls();
|
||||||
|
await eventSystem.Emit("ddos.add", blocklistIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnBlocklistIp(string ip)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||||
|
|
||||||
|
var blocklist = blocklistRepo.Get().First(x => x.Ip == ip);
|
||||||
|
blocklistRepo.Delete(blocklist);
|
||||||
|
|
||||||
|
await RebuildNodeFirewalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,11 +31,11 @@ public class DiscordNotificationService
|
|||||||
Client = new(config.WebHook);
|
Client = new(config.WebHook);
|
||||||
AppUrl = configService.Get().Moonlight.AppUrl;
|
AppUrl = configService.Get().Moonlight.AppUrl;
|
||||||
|
|
||||||
Event.On<User>("supportChat.new", this, OnNewSupportChat);
|
Event.On<Ticket>("tickets.new", this, OnNewTicket);
|
||||||
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);
|
Event.On<Ticket>("tickets.status", this, OnTicketStatusUpdated);
|
||||||
Event.On<User>("supportChat.close", this, OnSupportChatClose);
|
|
||||||
Event.On<User>("user.rating", this, OnUserRated);
|
Event.On<User>("user.rating", this, OnUserRated);
|
||||||
Event.On<User>("billing.completed", this, OnBillingCompleted);
|
Event.On<User>("billing.completed", this, OnBillingCompleted);
|
||||||
|
Event.On<BlocklistIp>("ddos.add", this, OnIpBlockListed);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -43,6 +43,36 @@ public class DiscordNotificationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnTicketStatusUpdated(Ticket ticket)
|
||||||
|
{
|
||||||
|
await SendNotification("", builder =>
|
||||||
|
{
|
||||||
|
builder.Title = "Ticket status has been updated";
|
||||||
|
builder.AddField("Issue topic", ticket.IssueTopic);
|
||||||
|
builder.AddField("Status", ticket.Status);
|
||||||
|
|
||||||
|
if (ticket.AssignedTo != null)
|
||||||
|
{
|
||||||
|
builder.AddField("Assigned to", $"{ticket.AssignedTo.FirstName} {ticket.AssignedTo.LastName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Color = Color.Green;
|
||||||
|
builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnIpBlockListed(BlocklistIp blocklistIp)
|
||||||
|
{
|
||||||
|
await SendNotification("", builder =>
|
||||||
|
{
|
||||||
|
builder.Color = Color.Red;
|
||||||
|
builder.Title = "New ddos attack detected";
|
||||||
|
|
||||||
|
builder.AddField("IP", blocklistIp.Ip);
|
||||||
|
builder.AddField("Packets", blocklistIp.Packets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnBillingCompleted(User user)
|
private async Task OnBillingCompleted(User user)
|
||||||
{
|
{
|
||||||
await SendNotification("", builder =>
|
await SendNotification("", builder =>
|
||||||
@@ -72,46 +102,14 @@ public class DiscordNotificationService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSupportChatClose(User user)
|
private async Task OnNewTicket(Ticket ticket)
|
||||||
{
|
{
|
||||||
await SendNotification("", builder =>
|
await SendNotification("", builder =>
|
||||||
{
|
{
|
||||||
builder.Title = "A new support chat has been marked as closed";
|
builder.Title = "A new ticket has been created";
|
||||||
builder.Color = Color.Red;
|
builder.AddField("Issue topic", ticket.IssueTopic);
|
||||||
builder.AddField("Email", user.Email);
|
|
||||||
builder.AddField("Firstname", user.FirstName);
|
|
||||||
builder.AddField("Lastname", user.LastName);
|
|
||||||
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSupportChatMessage(SupportChatMessage message)
|
|
||||||
{
|
|
||||||
if(message.Sender == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await SendNotification("", builder =>
|
|
||||||
{
|
|
||||||
builder.Title = "New message in support chat";
|
|
||||||
builder.Color = Color.Blue;
|
|
||||||
builder.AddField("Message", message.Content);
|
|
||||||
builder.Author = new EmbedAuthorBuilder()
|
|
||||||
.WithName($"{message.Sender.FirstName} {message.Sender.LastName}")
|
|
||||||
.WithIconUrl(ResourceService.Avatar(message.Sender));
|
|
||||||
builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnNewSupportChat(User user)
|
|
||||||
{
|
|
||||||
await SendNotification("", builder =>
|
|
||||||
{
|
|
||||||
builder.Title = "A new support chat has been marked as active";
|
|
||||||
builder.Color = Color.Green;
|
builder.Color = Color.Green;
|
||||||
builder.AddField("Email", user.Email);
|
builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}";
|
||||||
builder.AddField("Firstname", user.FirstName);
|
|
||||||
builder.AddField("Lastname", user.LastName);
|
|
||||||
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using Moonlight.App.ApiClients.Daemon.Resources;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Exceptions;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Background;
|
||||||
|
|
||||||
|
public class MalwareBackgroundScanService
|
||||||
|
{
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
|
public bool IsRunning => !ScanTask?.IsCompleted ?? false;
|
||||||
|
public bool ScanAllServers { get; set; }
|
||||||
|
public readonly Dictionary<Server, MalwareScanResult> ScanResults;
|
||||||
|
public string Status { get; private set; } = "N/A";
|
||||||
|
|
||||||
|
private Task? ScanTask;
|
||||||
|
|
||||||
|
public MalwareBackgroundScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
Event = eventSystem;
|
||||||
|
ScanResults = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start()
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
throw new DisplayException("Malware scan is already running");
|
||||||
|
|
||||||
|
ScanTask = Task.Run(Run);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Run()
|
||||||
|
{
|
||||||
|
// Clean results
|
||||||
|
Status = "Clearing last results";
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
|
||||||
|
lock (ScanResults)
|
||||||
|
ScanResults.Clear();
|
||||||
|
|
||||||
|
await Event.Emit("malwareScan.result");
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
||||||
|
var malwareScanService = scope.ServiceProvider.GetRequiredService<MalwareScanService>();
|
||||||
|
|
||||||
|
Status = "Fetching servers to scan";
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
|
||||||
|
Server[] servers;
|
||||||
|
|
||||||
|
if (ScanAllServers)
|
||||||
|
servers = serverRepo.Get().ToArray();
|
||||||
|
else
|
||||||
|
servers = await GetOnlineServers();
|
||||||
|
|
||||||
|
// Perform scan
|
||||||
|
|
||||||
|
int i = 1;
|
||||||
|
foreach (var server in servers)
|
||||||
|
{
|
||||||
|
Status = $"[{i} / {servers.Length}] Scanning server {server.Name}";
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
|
||||||
|
var result = await malwareScanService.Perform(server);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
lock (ScanResults)
|
||||||
|
{
|
||||||
|
ScanResults.Add(server, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Event.Emit("malwareScan.result", server);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () => // Because we use the task as the status indicator we need to notify the event system in a new task
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Server[]> GetOnlineServers()
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
// Load services from di scope
|
||||||
|
var nodeRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
|
||||||
|
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
||||||
|
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
|
||||||
|
|
||||||
|
var nodes = nodeRepo.Get().ToArray();
|
||||||
|
var containers = new List<Container>();
|
||||||
|
|
||||||
|
// Fetch and summarize all running containers from all nodes
|
||||||
|
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
|
||||||
|
|
||||||
|
Status = "Fetching and summarizing all running containers from all nodes";
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
var metrics = await nodeService.GetDockerMetrics(node);
|
||||||
|
|
||||||
|
foreach (var container in metrics.Containers)
|
||||||
|
{
|
||||||
|
containers.Add(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerServerMapped = new Dictionary<Server, Container>();
|
||||||
|
|
||||||
|
// Map all the containers to their corresponding server if existing
|
||||||
|
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
|
||||||
|
|
||||||
|
Status = "Mapping all the containers to their corresponding server if existing";
|
||||||
|
await Event.Emit("malwareScan.status", IsRunning);
|
||||||
|
|
||||||
|
foreach (var container in containers)
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(container.Name, out Guid uuid))
|
||||||
|
{
|
||||||
|
var server = serverRepo
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefault(x => x.Uuid == uuid);
|
||||||
|
|
||||||
|
if(server == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
containerServerMapped.Add(server, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerServerMapped.Keys.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
using Moonlight.App.ApiClients.Daemon.Resources;
|
|
||||||
using Moonlight.App.Database.Entities;
|
|
||||||
using Moonlight.App.Events;
|
|
||||||
using Moonlight.App.Exceptions;
|
|
||||||
using Moonlight.App.Helpers;
|
|
||||||
using Moonlight.App.Models.Misc;
|
|
||||||
using Moonlight.App.Repositories;
|
|
||||||
|
|
||||||
namespace Moonlight.App.Services.Background;
|
|
||||||
|
|
||||||
public class MalwareScanService
|
|
||||||
{
|
|
||||||
private Repository<Server> ServerRepository;
|
|
||||||
private Repository<Node> NodeRepository;
|
|
||||||
private NodeService NodeService;
|
|
||||||
private ServerService ServerService;
|
|
||||||
|
|
||||||
private readonly EventSystem Event;
|
|
||||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
|
||||||
|
|
||||||
public bool IsRunning { get; private set; }
|
|
||||||
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
|
|
||||||
public string Status { get; private set; } = "N/A";
|
|
||||||
|
|
||||||
public MalwareScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
|
|
||||||
{
|
|
||||||
ServiceScopeFactory = serviceScopeFactory;
|
|
||||||
Event = eventSystem;
|
|
||||||
|
|
||||||
ScanResults = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Start()
|
|
||||||
{
|
|
||||||
if (IsRunning)
|
|
||||||
throw new DisplayException("Malware scan is already running");
|
|
||||||
|
|
||||||
Task.Run(Run);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Run()
|
|
||||||
{
|
|
||||||
IsRunning = true;
|
|
||||||
Status = "Clearing last results";
|
|
||||||
await Event.Emit("malwareScan.status", IsRunning);
|
|
||||||
|
|
||||||
lock (ScanResults)
|
|
||||||
{
|
|
||||||
ScanResults.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Event.Emit("malwareScan.result");
|
|
||||||
|
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
|
||||||
|
|
||||||
// Load services from di scope
|
|
||||||
NodeRepository = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
|
|
||||||
ServerRepository = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
|
||||||
NodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
|
|
||||||
ServerService = scope.ServiceProvider.GetRequiredService<ServerService>();
|
|
||||||
|
|
||||||
var nodes = NodeRepository.Get().ToArray();
|
|
||||||
var containers = new List<Container>();
|
|
||||||
|
|
||||||
// Fetch and summarize all running containers from all nodes
|
|
||||||
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
|
|
||||||
|
|
||||||
Status = "Fetching and summarizing all running containers from all nodes";
|
|
||||||
await Event.Emit("malwareScan.status", IsRunning);
|
|
||||||
|
|
||||||
foreach (var node in nodes)
|
|
||||||
{
|
|
||||||
var metrics = await NodeService.GetDockerMetrics(node);
|
|
||||||
|
|
||||||
foreach (var container in metrics.Containers)
|
|
||||||
{
|
|
||||||
containers.Add(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var containerServerMapped = new Dictionary<Server, Container>();
|
|
||||||
|
|
||||||
// Map all the containers to their corresponding server if existing
|
|
||||||
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
|
|
||||||
|
|
||||||
Status = "Mapping all the containers to their corresponding server if existing";
|
|
||||||
await Event.Emit("malwareScan.status", IsRunning);
|
|
||||||
|
|
||||||
foreach (var container in containers)
|
|
||||||
{
|
|
||||||
if (Guid.TryParse(container.Name, out Guid uuid))
|
|
||||||
{
|
|
||||||
var server = ServerRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefault(x => x.Uuid == uuid);
|
|
||||||
|
|
||||||
if(server == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
containerServerMapped.Add(server, container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform scan
|
|
||||||
var resultsMapped = new Dictionary<Server, MalwareScanResult[]>();
|
|
||||||
foreach (var mapping in containerServerMapped)
|
|
||||||
{
|
|
||||||
Logger.Verbose($"Scanning server {mapping.Key.Name} for malware");
|
|
||||||
|
|
||||||
Status = $"Scanning server {mapping.Key.Name} for malware";
|
|
||||||
await Event.Emit("malwareScan.status", IsRunning);
|
|
||||||
|
|
||||||
var results = await PerformScanOnServer(mapping.Key, mapping.Value);
|
|
||||||
|
|
||||||
if (results.Any())
|
|
||||||
{
|
|
||||||
resultsMapped.Add(mapping.Key, results);
|
|
||||||
Logger.Verbose($"{results.Length} findings on server {mapping.Key.Name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Verbose($"Scan complete. Detected {resultsMapped.Count} servers with findings");
|
|
||||||
|
|
||||||
IsRunning = false;
|
|
||||||
Status = $"Scan complete. Detected {resultsMapped.Count} servers with findings";
|
|
||||||
await Event.Emit("malwareScan.status", IsRunning);
|
|
||||||
|
|
||||||
lock (ScanResults)
|
|
||||||
{
|
|
||||||
foreach (var mapping in resultsMapped)
|
|
||||||
{
|
|
||||||
ScanResults.Add(mapping.Key, mapping.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Event.Emit("malwareScan.result");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<MalwareScanResult[]> PerformScanOnServer(Server server, Container container)
|
|
||||||
{
|
|
||||||
var results = new List<MalwareScanResult>();
|
|
||||||
|
|
||||||
// TODO: Move scans to an universal format / api
|
|
||||||
|
|
||||||
// Define scans here
|
|
||||||
|
|
||||||
async Task ScanSelfBot()
|
|
||||||
{
|
|
||||||
var access = await ServerService.CreateFileAccess(server, null!);
|
|
||||||
var fileElements = await access.Ls();
|
|
||||||
|
|
||||||
if (fileElements.Any(x => x.Name == "tokens.txt"))
|
|
||||||
{
|
|
||||||
results.Add(new ()
|
|
||||||
{
|
|
||||||
Title = "Found SelfBot",
|
|
||||||
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
|
|
||||||
Author = "Marcel Baumgartner"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task ScanFakePlayerPlugins()
|
|
||||||
{
|
|
||||||
var access = await ServerService.CreateFileAccess(server, null!);
|
|
||||||
var fileElements = await access.Ls();
|
|
||||||
|
|
||||||
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
|
|
||||||
{
|
|
||||||
await access.Cd("plugins");
|
|
||||||
fileElements = await access.Ls();
|
|
||||||
|
|
||||||
foreach (var fileElement in fileElements)
|
|
||||||
{
|
|
||||||
if (fileElement.Name.ToLower().Contains("fake"))
|
|
||||||
{
|
|
||||||
results.Add(new()
|
|
||||||
{
|
|
||||||
Title = "Fake player plugin",
|
|
||||||
Description = $"Suspicious plugin file: {fileElement.Name}",
|
|
||||||
Author = "Marcel Baumgartner"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute scans
|
|
||||||
await ScanSelfBot();
|
|
||||||
await ScanFakePlayerPlugins();
|
|
||||||
|
|
||||||
return results.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
|
using Octokit;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.Files;
|
namespace Moonlight.App.Services.Files;
|
||||||
|
|
||||||
public class StorageService
|
public class StorageService
|
||||||
{
|
{
|
||||||
public StorageService()
|
public async Task EnsureCreated()
|
||||||
{
|
|
||||||
EnsureCreated();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnsureCreated()
|
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
||||||
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
||||||
|
|
||||||
if(IsEmpty(PathBuilder.Dir("storage", "resources")))
|
await UpdateResources();
|
||||||
|
|
||||||
|
return;
|
||||||
|
if (IsEmpty(PathBuilder.Dir("storage", "resources")))
|
||||||
{
|
{
|
||||||
Logger.Info("Default resources not found. Copying default resources");
|
Logger.Info("Default resources not found. Copying default resources");
|
||||||
|
|
||||||
@@ -38,10 +38,75 @@ public class StorageService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateResources()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.Info("Checking resources");
|
||||||
|
|
||||||
|
var client = new GitHubClient(
|
||||||
|
new ProductHeaderValue("Moonlight-Panel"));
|
||||||
|
|
||||||
|
var user = "Moonlight-Panel";
|
||||||
|
var repo = "Resources";
|
||||||
|
var resourcesDir = PathBuilder.Dir("storage", "resources");
|
||||||
|
|
||||||
|
async Task CopyDirectory(string dirPath, string localDir)
|
||||||
|
{
|
||||||
|
IReadOnlyList<RepositoryContent> contents;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(dirPath))
|
||||||
|
contents = await client.Repository.Content.GetAllContents(user, repo);
|
||||||
|
else
|
||||||
|
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
|
||||||
|
|
||||||
|
foreach (var content in contents)
|
||||||
|
{
|
||||||
|
string localPath = Path.Combine(localDir, content.Name);
|
||||||
|
|
||||||
|
if (content.Type == ContentType.File)
|
||||||
|
{
|
||||||
|
if (content.Name.EndsWith(".gitattributes"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (File.Exists(localPath) && !content.Name.EndsWith(".lang"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
|
||||||
|
new FileInfo(localPath).Length == content.Size)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
|
||||||
|
Directory.CreateDirectory(localDir); // Ensure the directory exists
|
||||||
|
await File.WriteAllBytesAsync(localPath, fileContent);
|
||||||
|
|
||||||
|
Logger.Debug($"Synced file '{content.Path}'");
|
||||||
|
}
|
||||||
|
else if (content.Type == ContentType.Dir)
|
||||||
|
{
|
||||||
|
await CopyDirectory(content.Path, localPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await CopyDirectory("", resourcesDir);
|
||||||
|
}
|
||||||
|
catch (RateLimitExceededException)
|
||||||
|
{
|
||||||
|
Logger.Warn("Unable to sync resources due to your ip being rate-limited by github");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warn("Unable to sync resources");
|
||||||
|
Logger.Warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsEmpty(string path)
|
private bool IsEmpty(string path)
|
||||||
{
|
{
|
||||||
return !Directory.EnumerateFileSystemEntries(path).Any();
|
return !Directory.EnumerateFileSystemEntries(path).Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CopyFilesRecursively(string sourcePath, string targetPath)
|
private static void CopyFilesRecursively(string sourcePath, string targetPath)
|
||||||
{
|
{
|
||||||
//Now Create all of the directories
|
//Now Create all of the directories
|
||||||
@@ -51,7 +116,7 @@ public class StorageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Copy all the files & Replaces any files with the same name
|
//Copy all the files & Replaces any files with the same name
|
||||||
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories))
|
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
|
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
|
||||||
}
|
}
|
||||||
|
|||||||
91
Moonlight/App/Services/IpVerificationService.cs
Normal file
91
Moonlight/App/Services/IpVerificationService.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Whois.NET;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services;
|
||||||
|
|
||||||
|
public class IpVerificationService
|
||||||
|
{
|
||||||
|
private readonly ConfigService ConfigService;
|
||||||
|
|
||||||
|
public IpVerificationService(ConfigService configService)
|
||||||
|
{
|
||||||
|
ConfigService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsDatacenterOrVpn(string ip)
|
||||||
|
{
|
||||||
|
if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ip))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var datacenterNames = new List<string>()
|
||||||
|
{
|
||||||
|
"amazon",
|
||||||
|
"aws",
|
||||||
|
"microsoft",
|
||||||
|
"azure",
|
||||||
|
"google",
|
||||||
|
"google cloud",
|
||||||
|
"gcp",
|
||||||
|
"digitalocean",
|
||||||
|
"linode",
|
||||||
|
"vultr",
|
||||||
|
"ovh",
|
||||||
|
"ovhcloud",
|
||||||
|
"alibaba",
|
||||||
|
"oracle",
|
||||||
|
"ibm cloud",
|
||||||
|
"bluehost",
|
||||||
|
"godaddy",
|
||||||
|
"rackpace",
|
||||||
|
"hetzner",
|
||||||
|
"tencent",
|
||||||
|
"scaleway",
|
||||||
|
"softlayer",
|
||||||
|
"dreamhost",
|
||||||
|
"a2 hosting",
|
||||||
|
"inmotion hosting",
|
||||||
|
"red hat openstack",
|
||||||
|
"kamatera",
|
||||||
|
"hostgator",
|
||||||
|
"siteground",
|
||||||
|
"greengeeks",
|
||||||
|
"liquidweb",
|
||||||
|
"joyent",
|
||||||
|
"aruba",
|
||||||
|
"interoute",
|
||||||
|
"fastcomet",
|
||||||
|
"rosehosting",
|
||||||
|
"lunarpages",
|
||||||
|
"fatcow",
|
||||||
|
"jelastic",
|
||||||
|
"datacamp"
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!ConfigService.Get().Moonlight.Security.AllowCloudflareIps)
|
||||||
|
datacenterNames.Add("cloudflare");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await WhoisClient.QueryAsync(ip);
|
||||||
|
var responseText = response.Raw.ToLower();
|
||||||
|
|
||||||
|
foreach (var name in datacenterNames)
|
||||||
|
{
|
||||||
|
if (responseText.Contains(name))
|
||||||
|
{
|
||||||
|
Logger.Debug(name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Moonlight/App/Services/MalwareScanService.cs
Normal file
46
Moonlight/App/Services/MalwareScanService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.MalwareScans;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services.Plugins;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services;
|
||||||
|
|
||||||
|
public class MalwareScanService
|
||||||
|
{
|
||||||
|
private readonly PluginService PluginService;
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
|
public MalwareScanService(PluginService pluginService, IServiceScopeFactory serviceScopeFactory)
|
||||||
|
{
|
||||||
|
PluginService = pluginService;
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MalwareScanResult?> Perform(Server server)
|
||||||
|
{
|
||||||
|
var defaultScans = new List<MalwareScan>
|
||||||
|
{
|
||||||
|
new SelfBotScan(),
|
||||||
|
new MinerJarScan(),
|
||||||
|
new SelfBotCodeScan(),
|
||||||
|
new FakePlayerPluginScan(),
|
||||||
|
new MinerScan(),
|
||||||
|
new ProxyScan(),
|
||||||
|
new DiscordNukeScan()
|
||||||
|
};
|
||||||
|
|
||||||
|
var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray());
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
foreach (var scan in scans)
|
||||||
|
{
|
||||||
|
var result = await scan.Scan(server, scope.ServiceProvider);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ public class MoonlightService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = new GitHubClient(new ProductHeaderValue("Moonlight"));
|
var client = new GitHubClient(new ProductHeaderValue("Moonlight-Panel"));
|
||||||
|
|
||||||
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
|
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ public class NodeService
|
|||||||
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
|
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RebuildFirewall(Node node, string[] ips)
|
||||||
|
{
|
||||||
|
await DaemonApiHelper.Post(node, "firewall/rebuild", ips);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Mount(Node node, string server, string serverPath, string path)
|
public async Task Mount(Node node, string server, string serverPath, string path)
|
||||||
{
|
{
|
||||||
await DaemonApiHelper.Post(node, "mount", new Mount()
|
await DaemonApiHelper.Post(node, "mount", new Mount()
|
||||||
|
|||||||
114
Moonlight/App/Services/Plugins/PluginService.cs
Normal file
114
Moonlight/App/Services/Plugins/PluginService.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Plugin;
|
||||||
|
using Moonlight.App.Plugin.UI.Servers;
|
||||||
|
using Moonlight.App.Plugin.UI.Webspaces;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Plugins;
|
||||||
|
|
||||||
|
public class PluginService
|
||||||
|
{
|
||||||
|
public readonly List<MoonlightPlugin> Plugins = new();
|
||||||
|
public readonly Dictionary<MoonlightPlugin, string> PluginFiles = new();
|
||||||
|
|
||||||
|
public PluginService()
|
||||||
|
{
|
||||||
|
ReloadPlugins().Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReloadPlugins()
|
||||||
|
{
|
||||||
|
PluginFiles.Clear();
|
||||||
|
Plugins.Clear();
|
||||||
|
|
||||||
|
// Try to update all plugins ending with .dll.cache
|
||||||
|
foreach (var pluginFile in Directory.EnumerateFiles(
|
||||||
|
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
|
||||||
|
.Where(x => x.EndsWith(".dll.cache")))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var realPath = pluginFile.Replace(".cache", "");
|
||||||
|
File.Copy(pluginFile, realPath, true);
|
||||||
|
File.Delete(pluginFile);
|
||||||
|
Logger.Info($"Updated plugin {realPath} on startup");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginType = typeof(MoonlightPlugin);
|
||||||
|
|
||||||
|
foreach (var pluginFile in Directory.EnumerateFiles(
|
||||||
|
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
|
||||||
|
.Where(x => x.EndsWith(".dll")))
|
||||||
|
{
|
||||||
|
var assembly = Assembly.LoadFile(pluginFile);
|
||||||
|
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (type.IsSubclassOf(pluginType))
|
||||||
|
{
|
||||||
|
var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!;
|
||||||
|
|
||||||
|
Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}");
|
||||||
|
|
||||||
|
Plugins.Add(plugin);
|
||||||
|
PluginFiles.Add(plugin, pluginFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info($"Loaded {Plugins.Count} plugins");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServerPageContext> BuildServerPage(ServerPageContext context)
|
||||||
|
{
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
if (plugin.OnBuildServerPage != null)
|
||||||
|
await plugin.OnBuildServerPage.Invoke(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WebspacePageContext> BuildWebspacePage(WebspacePageContext context)
|
||||||
|
{
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
if (plugin.OnBuildWebspacePage != null)
|
||||||
|
await plugin.OnBuildWebspacePage.Invoke(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BuildServices(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
if (plugin.OnBuildServices != null)
|
||||||
|
await plugin.OnBuildServices.Invoke(serviceCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MalwareScan[]> BuildMalwareScans(MalwareScan[] defaultScans)
|
||||||
|
{
|
||||||
|
var scanList = defaultScans.ToList();
|
||||||
|
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
if (plugin.OnBuildMalwareScans != null)
|
||||||
|
scanList = await plugin.OnBuildMalwareScans.Invoke(scanList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanList.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Moonlight/App/Services/Plugins/PluginStoreService.cs
Normal file
63
Moonlight/App/Services/Plugins/PluginStoreService.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Octokit;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Plugins;
|
||||||
|
|
||||||
|
public class PluginStoreService
|
||||||
|
{
|
||||||
|
private readonly GitHubClient Client;
|
||||||
|
private readonly PluginService PluginService;
|
||||||
|
|
||||||
|
public PluginStoreService(PluginService pluginService)
|
||||||
|
{
|
||||||
|
PluginService = pluginService;
|
||||||
|
Client = new(new ProductHeaderValue("Moonlight-Panel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OfficialMoonlightPlugin[]> GetPlugins()
|
||||||
|
{
|
||||||
|
var items = await Client.Repository.Content.GetAllContents("Moonlight-Panel", "OfficialPlugins");
|
||||||
|
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
Logger.Fatal("Unable to read plugin repo contents");
|
||||||
|
return Array.Empty<OfficialMoonlightPlugin>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
.Where(x => x.Type == ContentType.Dir)
|
||||||
|
.Select(x => new OfficialMoonlightPlugin()
|
||||||
|
{
|
||||||
|
Name = x.Name
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetPluginReadme(OfficialMoonlightPlugin plugin)
|
||||||
|
{
|
||||||
|
var rawReadme = await Client.Repository.Content
|
||||||
|
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/README.md");
|
||||||
|
|
||||||
|
if (rawReadme == null)
|
||||||
|
return "Error";
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(rawReadme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InstallPlugin(OfficialMoonlightPlugin plugin, bool updating = false)
|
||||||
|
{
|
||||||
|
var rawPlugin = await Client.Repository.Content
|
||||||
|
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/{plugin.Name}.dll");
|
||||||
|
|
||||||
|
if (updating)
|
||||||
|
{
|
||||||
|
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll.cache"), rawPlugin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll"), rawPlugin);
|
||||||
|
await PluginService.ReloadPlugins();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ using Moonlight.App.Helpers.Wings;
|
|||||||
using Moonlight.App.Models.Misc;
|
using Moonlight.App.Models.Misc;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Repositories.Servers;
|
using Moonlight.App.Repositories.Servers;
|
||||||
|
using Moonlight.App.Services.Background;
|
||||||
|
using Moonlight.App.Services.Plugins;
|
||||||
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
|
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
|
||||||
|
|
||||||
namespace Moonlight.App.Services;
|
namespace Moonlight.App.Services;
|
||||||
@@ -32,6 +34,11 @@ public class ServerService
|
|||||||
private readonly DateTimeService DateTimeService;
|
private readonly DateTimeService DateTimeService;
|
||||||
private readonly EventSystem Event;
|
private readonly EventSystem Event;
|
||||||
|
|
||||||
|
// We inject the dependencies for the malware scan service here because otherwise it may result in
|
||||||
|
// a circular dependency injection which will crash the app
|
||||||
|
private readonly PluginService PluginService;
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
public ServerService(
|
public ServerService(
|
||||||
ServerRepository serverRepository,
|
ServerRepository serverRepository,
|
||||||
WingsApiHelper wingsApiHelper,
|
WingsApiHelper wingsApiHelper,
|
||||||
@@ -45,7 +52,9 @@ public class ServerService
|
|||||||
NodeAllocationRepository nodeAllocationRepository,
|
NodeAllocationRepository nodeAllocationRepository,
|
||||||
DateTimeService dateTimeService,
|
DateTimeService dateTimeService,
|
||||||
EventSystem eventSystem,
|
EventSystem eventSystem,
|
||||||
Repository<ServerVariable> serverVariablesRepository)
|
Repository<ServerVariable> serverVariablesRepository,
|
||||||
|
PluginService pluginService,
|
||||||
|
IServiceScopeFactory serviceScopeFactory)
|
||||||
{
|
{
|
||||||
ServerRepository = serverRepository;
|
ServerRepository = serverRepository;
|
||||||
WingsApiHelper = wingsApiHelper;
|
WingsApiHelper = wingsApiHelper;
|
||||||
@@ -60,6 +69,8 @@ public class ServerService
|
|||||||
DateTimeService = dateTimeService;
|
DateTimeService = dateTimeService;
|
||||||
Event = eventSystem;
|
Event = eventSystem;
|
||||||
ServerVariablesRepository = serverVariablesRepository;
|
ServerVariablesRepository = serverVariablesRepository;
|
||||||
|
PluginService = pluginService;
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Server EnsureNodeData(Server s)
|
private Server EnsureNodeData(Server s)
|
||||||
@@ -99,6 +110,24 @@ public class ServerService
|
|||||||
{
|
{
|
||||||
Server server = EnsureNodeData(s);
|
Server server = EnsureNodeData(s);
|
||||||
|
|
||||||
|
if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start ||
|
||||||
|
signal == PowerSignal.Restart)
|
||||||
|
{
|
||||||
|
var result = await new MalwareScanService(
|
||||||
|
PluginService,
|
||||||
|
ServiceScopeFactory
|
||||||
|
).Perform(server);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title, "security");
|
||||||
|
|
||||||
|
throw new DisplayException(
|
||||||
|
$"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive",
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rawSignal = signal.ToString().ToLower();
|
var rawSignal = signal.ToString().ToLower();
|
||||||
|
|
||||||
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower()
|
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower()
|
||||||
@@ -420,10 +449,7 @@ public class ServerService
|
|||||||
await new Retry()
|
await new Retry()
|
||||||
.Times(3)
|
.Times(3)
|
||||||
.At(x => x.Message.Contains("A task was canceled"))
|
.At(x => x.Message.Contains("A task was canceled"))
|
||||||
.Call(async () =>
|
.Call(async () => { await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null); });
|
||||||
{
|
|
||||||
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (WingsException e)
|
catch (WingsException e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public class IdentityService
|
|||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
Logger.Warn(
|
Logger.Warn(
|
||||||
$"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker");
|
$"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker", "security");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
67
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services.Files;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketAdminService
|
||||||
|
{
|
||||||
|
private readonly TicketServerService TicketServerService;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
|
public Ticket? Ticket { get; set; }
|
||||||
|
|
||||||
|
public TicketAdminService(
|
||||||
|
TicketServerService ticketServerService,
|
||||||
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
|
{
|
||||||
|
TicketServerService = ticketServerService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage> Send(string content, IBrowserFile? file = null)
|
||||||
|
{
|
||||||
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"tickets",
|
||||||
|
file.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
file.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await TicketServerService.SendMessage(
|
||||||
|
Ticket!,
|
||||||
|
IdentityService.User,
|
||||||
|
content,
|
||||||
|
attachment,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatus(TicketStatus status)
|
||||||
|
{
|
||||||
|
await TicketServerService.UpdateStatus(Ticket!, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePriority(TicketPriority priority)
|
||||||
|
{
|
||||||
|
await TicketServerService.UpdatePriority(Ticket!, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetMessages(Ticket!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetClaim(User? user)
|
||||||
|
{
|
||||||
|
await TicketServerService.SetClaim(Ticket!, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
63
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services.Files;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketClientService
|
||||||
|
{
|
||||||
|
private readonly TicketServerService TicketServerService;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
|
public Ticket? Ticket { get; set; }
|
||||||
|
|
||||||
|
public TicketClientService(
|
||||||
|
TicketServerService ticketServerService,
|
||||||
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
|
{
|
||||||
|
TicketServerService = ticketServerService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Create(string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId)
|
||||||
|
{
|
||||||
|
return await TicketServerService.Create(
|
||||||
|
IdentityService.User,
|
||||||
|
issueTopic,
|
||||||
|
issueDescription,
|
||||||
|
issueTries,
|
||||||
|
subject,
|
||||||
|
subjectId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage> Send(string content, IBrowserFile? file = null)
|
||||||
|
{
|
||||||
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"tickets",
|
||||||
|
file.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
file.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await TicketServerService.SendMessage(
|
||||||
|
Ticket!,
|
||||||
|
IdentityService.User,
|
||||||
|
content,
|
||||||
|
attachment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetMessages(Ticket!);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
181
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketServerService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly ConfigService ConfigService;
|
||||||
|
|
||||||
|
public TicketServerService(
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
EventSystem eventSystem,
|
||||||
|
ConfigService configService)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
Event = eventSystem;
|
||||||
|
ConfigService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Create(User creator, string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var creatorUser = userRepo
|
||||||
|
.Get()
|
||||||
|
.First(x => x.Id == creator.Id);
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Add(new()
|
||||||
|
{
|
||||||
|
Priority = TicketPriority.Low,
|
||||||
|
Status = TicketStatus.Open,
|
||||||
|
AssignedTo = null,
|
||||||
|
IssueTopic = issueTopic,
|
||||||
|
IssueDescription = issueDescription,
|
||||||
|
IssueTries = issueTries,
|
||||||
|
Subject = subject,
|
||||||
|
SubjectId = subjectId,
|
||||||
|
CreatedBy = creatorUser
|
||||||
|
});
|
||||||
|
|
||||||
|
await Event.Emit("tickets.new", ticket);
|
||||||
|
|
||||||
|
// Do automatic stuff here
|
||||||
|
await SendSystemMessage(ticket, ConfigService.Get().Moonlight.Tickets.WelcomeMessage);
|
||||||
|
|
||||||
|
if (ticket.Subject != TicketSubject.Other)
|
||||||
|
{
|
||||||
|
await SendMessage(ticket, creatorUser, $"Subject :\n\n{ticket.Subject}: {ticket.SubjectId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Check for opening times
|
||||||
|
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
public async Task SendSystemMessage(Ticket t, string content, string? attachmentUrl = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Sender = null,
|
||||||
|
AttachmentUrl = attachmentUrl,
|
||||||
|
IsSystemMessage = true
|
||||||
|
};
|
||||||
|
|
||||||
|
ticket.Messages.Add(message);
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.message", message);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.message", message);
|
||||||
|
}
|
||||||
|
public async Task UpdatePriority(Ticket t, TicketPriority priority)
|
||||||
|
{
|
||||||
|
if(t.Priority == priority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
ticket.Priority = priority;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
|
||||||
|
await SendSystemMessage(ticket, $"The ticket priority has been changed to: {priority}");
|
||||||
|
}
|
||||||
|
public async Task UpdateStatus(Ticket t, TicketStatus status)
|
||||||
|
{
|
||||||
|
if(t.Status == status)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
ticket.Status = status;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
|
||||||
|
await SendSystemMessage(ticket, $"The ticket status has been changed to: {status}");
|
||||||
|
}
|
||||||
|
public async Task<TicketMessage> SendMessage(Ticket t, User sender, string content, string? attachmentUrl = null, bool isSupportMessage = false)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
var user = userRepo.Get().First(x => x.Id == sender.Id);
|
||||||
|
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Sender = user,
|
||||||
|
AttachmentUrl = attachmentUrl,
|
||||||
|
IsSupportMessage = isSupportMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
ticket.Messages.Add(message);
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.message", message);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.message", message);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TicketMessage[]> GetMessages(Ticket ticket)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var tickets = ticketRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.ThenInclude(x => x.Sender)
|
||||||
|
.First(x => x.Id == ticket.Id);
|
||||||
|
|
||||||
|
return Task.FromResult(tickets.Messages.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetClaim(Ticket t, User? u = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().Include(x => x.AssignedTo).First(x => x.Id == t.Id);
|
||||||
|
var user = u == null ? u : userRepo.Get().First(x => x.Id == u.Id);
|
||||||
|
|
||||||
|
ticket.AssignedTo = user;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
|
||||||
|
var claimName = user == null ? "None" : user.FirstName + " " + user.LastName;
|
||||||
|
await SendSystemMessage(ticket, $"Ticked claim has been set to {claimName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ public class UserService
|
|||||||
private readonly DateTimeService DateTimeService;
|
private readonly DateTimeService DateTimeService;
|
||||||
private readonly ConfigService ConfigService;
|
private readonly ConfigService ConfigService;
|
||||||
private readonly TempMailService TempMailService;
|
private readonly TempMailService TempMailService;
|
||||||
|
private readonly MoonlightService MoonlightService;
|
||||||
|
|
||||||
private readonly string JwtSecret;
|
private readonly string JwtSecret;
|
||||||
|
|
||||||
@@ -32,7 +33,8 @@ public class UserService
|
|||||||
IdentityService identityService,
|
IdentityService identityService,
|
||||||
IpLocateService ipLocateService,
|
IpLocateService ipLocateService,
|
||||||
DateTimeService dateTimeService,
|
DateTimeService dateTimeService,
|
||||||
TempMailService tempMailService)
|
TempMailService tempMailService,
|
||||||
|
MoonlightService moonlightService)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
TotpService = totpService;
|
TotpService = totpService;
|
||||||
@@ -42,6 +44,7 @@ public class UserService
|
|||||||
IpLocateService = ipLocateService;
|
IpLocateService = ipLocateService;
|
||||||
DateTimeService = dateTimeService;
|
DateTimeService = dateTimeService;
|
||||||
TempMailService = tempMailService;
|
TempMailService = tempMailService;
|
||||||
|
MoonlightService = moonlightService;
|
||||||
|
|
||||||
JwtSecret = configService
|
JwtSecret = configService
|
||||||
.Get()
|
.Get()
|
||||||
@@ -67,16 +70,24 @@ public class UserService
|
|||||||
throw new DisplayException("The email is already in use");
|
throw new DisplayException("The email is already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Validation
|
bool admin = false;
|
||||||
|
|
||||||
|
if (!UserRepository.Get().Any())
|
||||||
|
{
|
||||||
|
if ((DateTime.UtcNow - MoonlightService.StartTimestamp).TotalMinutes < 15)
|
||||||
|
admin = true;
|
||||||
|
else
|
||||||
|
throw new DisplayException("You have to register within 15 minutes after the start of moonlight to get admin permissions. Please restart moonlight in order to register as admin. Please note that this will only works once and will be deactivated after a admin has registered");
|
||||||
|
}
|
||||||
|
|
||||||
// Add user
|
// Add user
|
||||||
var user = UserRepository.Add(new()
|
var user = UserRepository.Add(new()
|
||||||
{
|
{
|
||||||
Address = "",
|
Address = "",
|
||||||
Admin = false,
|
Admin = admin,
|
||||||
City = "",
|
City = "",
|
||||||
Country = "",
|
Country = "",
|
||||||
Email = email,
|
Email = email.ToLower(),
|
||||||
Password = BCrypt.Net.BCrypt.HashPassword(password),
|
Password = BCrypt.Net.BCrypt.HashPassword(password),
|
||||||
FirstName = firstname,
|
FirstName = firstname,
|
||||||
LastName = lastname,
|
LastName = lastname,
|
||||||
@@ -108,7 +119,7 @@ public class UserService
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Failed login attempt. Email: {email} Password: {password}", "security");
|
Logger.Warn($"Failed login attempt. Email: {email} Password: {StringHelper.CutInHalf(password)}", "security");
|
||||||
throw new DisplayException("Email and password combination not found");
|
throw new DisplayException("Email and password combination not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +128,7 @@ public class UserService
|
|||||||
return user.TotpEnabled;
|
return user.TotpEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Warn($"Failed login attempt. Email: {email} Password: {password}", "security");
|
Logger.Warn($"Failed login attempt. Email: {email} Password: {StringHelper.CutInHalf(password)}", "security");
|
||||||
throw new DisplayException("Email and password combination not found");;
|
throw new DisplayException("Email and password combination not found");;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +161,7 @@ public class UserService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warn($"Failed login attempt. Email: {email} Password: {password}", "security");
|
Logger.Warn($"Failed login attempt. Email: {email} Password: {StringHelper.CutInHalf(password)}", "security");
|
||||||
throw new DisplayException("2FA code invalid");
|
throw new DisplayException("2FA code invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +203,7 @@ public class UserService
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {password}", "security");
|
Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {StringHelper.CutInHalf(password)}", "security");
|
||||||
|
|
||||||
throw new Exception("Invalid username");
|
throw new Exception("Invalid username");
|
||||||
}
|
}
|
||||||
@@ -203,7 +214,7 @@ public class UserService
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {password}", "security");
|
Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {StringHelper.CutInHalf(password)}", "security");
|
||||||
throw new Exception("Invalid userid or password");
|
throw new Exception("Invalid userid or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,4 @@ COPY --from=publish /app/publish .
|
|||||||
RUN mkdir -p /app/storage
|
RUN mkdir -p /app/storage
|
||||||
RUN touch /app/storage/donttriggeranyerrors
|
RUN touch /app/storage/donttriggeranyerrors
|
||||||
RUN rm -r /app/storage/*
|
RUN rm -r /app/storage/*
|
||||||
COPY "Moonlight/defaultstorage" "/app/defaultstorage"
|
|
||||||
ENTRYPOINT ["dotnet", "Moonlight.dll"]
|
ENTRYPOINT ["dotnet", "Moonlight.dll"]
|
||||||
@@ -40,12 +40,12 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MineStat" Version="3.1.1" />
|
<PackageReference Include="MineStat" Version="3.1.1" />
|
||||||
<PackageReference Include="MySqlBackup.NET" Version="2.3.8" />
|
<PackageReference Include="MySqlBackup.NET" Version="2.3.8" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3-beta1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
<PackageReference Include="Octokit" Version="6.0.0" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
|
<PackageReference Include="RestSharp" Version="110.2.1-alpha.0.10" />
|
||||||
<PackageReference Include="Sentry.AspNetCore" Version="3.33.1" />
|
<PackageReference Include="Sentry.AspNetCore" Version="3.33.1" />
|
||||||
<PackageReference Include="Sentry.Serilog" Version="3.33.1" />
|
<PackageReference Include="Sentry.Serilog" Version="3.33.1" />
|
||||||
<PackageReference Include="Serilog" Version="3.0.0" />
|
<PackageReference Include="Serilog" Version="3.0.0" />
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||||
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
|
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
|
||||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||||
|
<PackageReference Include="WhoisClient.NET" Version="5.0.0" />
|
||||||
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -84,6 +85,103 @@
|
|||||||
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryChangePowerState.razor" />
|
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryChangePowerState.razor" />
|
||||||
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryLogin.razor" />
|
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryLogin.razor" />
|
||||||
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryRegister.razor" />
|
<_ContentIncludedByDefault Remove="Shared\Components\AuditLogEntrys\AuditLogEntryRegister.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\ckeditor\ckeditor-balloon-block.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\ckeditor\ckeditor-balloon.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\ckeditor\ckeditor-classic.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\ckeditor\ckeditor-document.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\ckeditor\ckeditor-inline.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\cookiealert\cookiealert.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\cookiealert\cookiealert.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\cropper\cropper.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\cropper\cropper.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\datatables\datatables.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\datatables\datatables.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\draggable\draggable.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\flotcharts\flotcharts.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\formrepeater\formrepeater.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\fslightbox\fslightbox.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\fullcalendar\fullcalendar.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\fullcalendar\fullcalendar.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jkanban\jkanban.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jkanban\jkanban.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jstree\images\jstree\32px.png" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jstree\images\jstree\throbber.gif" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jstree\jstree.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\jstree\jstree.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\leaflet\leaflet.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\leaflet\leaflet.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\prismjs\prismjs.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\prismjs\prismjs.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\dark\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\dark\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\default\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\default\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\document\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\document\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\writer\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\content\writer\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.inline.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.inline.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.mobile.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\content.mobile.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.mobile.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.mobile.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.shadowdom.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide-dark\skin.shadowdom.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.inline.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.inline.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.mobile.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\content.mobile.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.mobile.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.mobile.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.shadowdom.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\skins\ui\oxide\skin.shadowdom.min.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\tinymce\tinymce.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\typedjs\typedjs.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\vis-timeline\vis-timeline.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\custom\vis-timeline\vis-timeline.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-brands-400.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-brands-400.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-regular-400.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-regular-400.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-solid-900.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-solid-900.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-v4compatibility.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\@fortawesome\fa-v4compatibility.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\bootstrap-icons\bootstrap-icons.woff" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\bootstrap-icons\bootstrap-icons.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.eot" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.svg" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.woff" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\fonticon\fonticon.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-brands-400.eot" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-brands-400.svg" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-brands-400.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-brands-400.woff" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-brands-400.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-regular-400.eot" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-regular-400.svg" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-regular-400.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-regular-400.woff" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-regular-400.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-solid-900.eot" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-solid-900.svg" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-solid-900.ttf" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-solid-900.woff" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\fonts\line-awesome\la-solid-900.woff2" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\plugins.bundle.css" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\plugins.bundle.js" />
|
||||||
|
<_ContentIncludedByDefault Remove="wwwroot\assets\plugins\global\sourcemaps\tiny-slider.css.map" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -111,12 +209,4 @@
|
|||||||
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerResetSetting.razor" />
|
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerResetSetting.razor" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Update="storage\configs\config.json.bak">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using Moonlight.App.Extensions
|
@using Moonlight.App.Extensions
|
||||||
@using Moonlight.App.Repositories
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/style.bundle.css"/>
|
<link rel="stylesheet" type="text/css" href="/assets/css/style.bundle.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/flashbang.css"/>
|
<link rel="stylesheet" type="text/css" href="/assets/css/flashbang.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/snow.css"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/utils.css"/>
|
<link rel="stylesheet" type="text/css" href="/assets/css/utils.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/boxicons.min.css"/>
|
<link rel="stylesheet" type="text/css" href="/assets/css/boxicons.min.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/blazor.css"/>
|
<link rel="stylesheet" type="text/css" href="/assets/css/blazor.css"/>
|
||||||
@@ -49,8 +48,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
|
<link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css"/>
|
<link rel="stylesheet" type="text/css" href="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/_content/Blazor.ContextMenu/blazorContextMenu.min.css"/>
|
<link rel="stylesheet" type="text/css" href="/_content/Blazor.ContextMenu/blazorContextMenu.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/css/toastr.css"/>
|
||||||
<link href="/assets/plugins/global/plugins.bundle.css" rel="stylesheet" type="text/css"/>
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<base href="~/"/>
|
<base href="~/"/>
|
||||||
@@ -96,31 +94,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/plugins/global/plugins.bundle.js"></script>
|
<script src="/assets/js/popper.min.js"></script>
|
||||||
|
<script src="/assets/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
|
||||||
|
<script src="/_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
|
||||||
|
|
||||||
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
|
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
|
||||||
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
|
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
|
||||||
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
|
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
|
||||||
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
|
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@@shopify/draggable@1.0.0-beta.11/lib/draggable.bundle.js"></script>
|
<script src="/assets/js/draggable.bundle.js"></script>
|
||||||
|
|
||||||
<script src="https://www.google.com/recaptcha/api.js"></script>
|
<script src="https://www.google.com/recaptcha/api.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>
|
<script src="/assets/js/xterm-addon-fit.min.js"></script>
|
||||||
|
<script src="/assets/js/jquery.min.js"></script>
|
||||||
|
<script src="/assets/js/toastr.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/assets/js/apexcharts.js"></script>
|
||||||
|
|
||||||
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
|
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
|
||||||
<script>require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
|
<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/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
|
||||||
<script src="/_content/BlazorMonaco/jsInterop.js"></script>
|
<script src="/_content/BlazorMonaco/jsInterop.js"></script>
|
||||||
|
|
||||||
<script src="/assets/js/scripts.bundle.js"></script>
|
|
||||||
<script src="/assets/js/moonlight.js"></script>
|
<script src="/assets/js/moonlight.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
moonlight.loading.registerXterm();
|
moonlight.loading.registerXterm();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
|
|
||||||
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
|
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
|
||||||
|
|
||||||
<script src="/_framework/blazor.server.js"></script>
|
<script src="/_framework/blazor.server.js"></script>
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ using Moonlight.App.Services.Interop;
|
|||||||
using Moonlight.App.Services.Mail;
|
using Moonlight.App.Services.Mail;
|
||||||
using Moonlight.App.Services.Minecraft;
|
using Moonlight.App.Services.Minecraft;
|
||||||
using Moonlight.App.Services.Notifications;
|
using Moonlight.App.Services.Notifications;
|
||||||
|
using Moonlight.App.Services.Plugins;
|
||||||
using Moonlight.App.Services.Sessions;
|
using Moonlight.App.Services.Sessions;
|
||||||
using Moonlight.App.Services.Statistics;
|
using Moonlight.App.Services.Statistics;
|
||||||
using Moonlight.App.Services.SupportChat;
|
using Moonlight.App.Services.SupportChat;
|
||||||
|
using Moonlight.App.Services.Tickets;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
@@ -40,8 +42,10 @@ namespace Moonlight
|
|||||||
{
|
{
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
// This will also copy all default config files
|
var storageService = new StorageService();
|
||||||
var configService = new ConfigService(new StorageService());
|
await storageService.EnsureCreated();
|
||||||
|
|
||||||
|
var configService = new ConfigService(storageService);
|
||||||
var shouldUseSentry = configService
|
var shouldUseSentry = configService
|
||||||
.Get()
|
.Get()
|
||||||
.Moonlight.Sentry.Enable;
|
.Moonlight.Sentry.Enable;
|
||||||
@@ -110,6 +114,9 @@ namespace Moonlight
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var pluginService = new PluginService();
|
||||||
|
await pluginService.BuildServices(builder.Services);
|
||||||
|
|
||||||
// Switch to logging.net injection
|
// Switch to logging.net injection
|
||||||
// TODO: Enable in production
|
// TODO: Enable in production
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
@@ -208,6 +215,12 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<PopupService>();
|
builder.Services.AddScoped<PopupService>();
|
||||||
builder.Services.AddScoped<SubscriptionService>();
|
builder.Services.AddScoped<SubscriptionService>();
|
||||||
builder.Services.AddScoped<BillingService>();
|
builder.Services.AddScoped<BillingService>();
|
||||||
|
builder.Services.AddSingleton<PluginStoreService>();
|
||||||
|
builder.Services.AddSingleton<TicketServerService>();
|
||||||
|
builder.Services.AddScoped<TicketClientService>();
|
||||||
|
builder.Services.AddScoped<TicketAdminService>();
|
||||||
|
builder.Services.AddScoped<MalwareScanService>();
|
||||||
|
builder.Services.AddSingleton<IpVerificationService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SessionClientService>();
|
builder.Services.AddScoped<SessionClientService>();
|
||||||
builder.Services.AddSingleton<SessionServerService>();
|
builder.Services.AddSingleton<SessionServerService>();
|
||||||
@@ -236,9 +249,11 @@ namespace Moonlight
|
|||||||
builder.Services.AddSingleton<StatisticsCaptureService>();
|
builder.Services.AddSingleton<StatisticsCaptureService>();
|
||||||
builder.Services.AddSingleton<DiscordNotificationService>();
|
builder.Services.AddSingleton<DiscordNotificationService>();
|
||||||
builder.Services.AddSingleton<CleanupService>();
|
builder.Services.AddSingleton<CleanupService>();
|
||||||
builder.Services.AddSingleton<MalwareScanService>();
|
builder.Services.AddSingleton<MalwareBackgroundScanService>();
|
||||||
builder.Services.AddSingleton<TelemetryService>();
|
builder.Services.AddSingleton<TelemetryService>();
|
||||||
builder.Services.AddSingleton<TempMailService>();
|
builder.Services.AddSingleton<TempMailService>();
|
||||||
|
builder.Services.AddSingleton<DdosProtectionService>();
|
||||||
|
builder.Services.AddSingleton(pluginService);
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
builder.Services.AddSingleton<MoonlightService>();
|
builder.Services.AddSingleton<MoonlightService>();
|
||||||
@@ -286,10 +301,10 @@ namespace Moonlight
|
|||||||
_ = app.Services.GetRequiredService<DiscordBotService>();
|
_ = app.Services.GetRequiredService<DiscordBotService>();
|
||||||
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
|
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
|
||||||
_ = app.Services.GetRequiredService<DiscordNotificationService>();
|
_ = app.Services.GetRequiredService<DiscordNotificationService>();
|
||||||
_ = app.Services.GetRequiredService<MalwareScanService>();
|
_ = app.Services.GetRequiredService<MalwareBackgroundScanService>();
|
||||||
_ = app.Services.GetRequiredService<TelemetryService>();
|
_ = app.Services.GetRequiredService<TelemetryService>();
|
||||||
_ = app.Services.GetRequiredService<TempMailService>();
|
_ = app.Services.GetRequiredService<TempMailService>();
|
||||||
|
_ = app.Services.GetRequiredService<DdosProtectionService>();
|
||||||
_ = app.Services.GetRequiredService<MoonlightService>();
|
_ = app.Services.GetRequiredService<MoonlightService>();
|
||||||
|
|
||||||
// Discord bot service
|
// Discord bot service
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ML_DEBUG": "true"
|
"ML_DEBUG": "true"
|
||||||
},
|
},
|
||||||
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
|
"applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
|
||||||
"dotnetRunMessages": true
|
"dotnetRunMessages": true
|
||||||
},
|
},
|
||||||
"Live DB":
|
"Live DB":
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"ML_DEBUG": "true",
|
"ML_DEBUG": "true",
|
||||||
"ML_CONFIG_PATH": "storage\\configs\\live_config.json"
|
"ML_CONFIG_PATH": "storage\\configs\\live_config.json"
|
||||||
},
|
},
|
||||||
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
|
"applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
|
||||||
"dotnetRunMessages": true
|
"dotnetRunMessages": true
|
||||||
},
|
},
|
||||||
"Dev DB 1":
|
"Dev DB 1":
|
||||||
@@ -33,7 +33,19 @@
|
|||||||
"ML_DEBUG": "true",
|
"ML_DEBUG": "true",
|
||||||
"ML_CONFIG_PATH": "storage\\configs\\dev_1_config.json"
|
"ML_CONFIG_PATH": "storage\\configs\\dev_1_config.json"
|
||||||
},
|
},
|
||||||
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
|
"applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
|
||||||
|
"dotnetRunMessages": true
|
||||||
|
},
|
||||||
|
"Dev DB 2":
|
||||||
|
{
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ML_DEBUG": "true",
|
||||||
|
"ML_CONFIG_PATH": "storage\\configs\\dev_2_config.json"
|
||||||
|
},
|
||||||
|
"applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
|
||||||
"dotnetRunMessages": true
|
"dotnetRunMessages": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
### Some explanations
|
# Some explanations
|
||||||
|
|
||||||
defaultstorage:
|
defaultstorage:
|
||||||
|
|
||||||
This directory is for the default assets of a moonlight instance, e.g. lang files, images etc
|
This directory is for the default assets of a Moonlight instance, e.g., Lang files, images, etc.
|
||||||
|
|
||||||
storage:
|
Storage:
|
||||||
|
|
||||||
This directory is empty in fresh moonlight instances and will be populated with example config upon first run.
|
This directory is empty in fresh Moonlight instances and will be populated with an example configuration upon first run. Before using Moonlight, this config file has to be modified. Also, resources are going to be copied from the default storage to this storage. To access files in this storage, we recommend using the Path Builder functions to ensure cross-platform compatibility. The storage directory should be mounted to a specific path when using a Docker container, so when the container is replaced or rebuilt, your storage will not be modified.
|
||||||
Before using moonlight this config file has to be modified.
|
|
||||||
Also resources are going to be copied from the default storage to this storage.
|
|
||||||
To access files in this storage we recommend to use the PathBuilder functions to ensure cross platform compatibility.
|
|
||||||
The storage directory should be mounted to a specific path when using docker container so when the container is replaced/rebuild your storage will not be modified
|
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ConfigService ConfigService
|
@inject ConfigService ConfigService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
@if (Crashed)
|
@if (HardCrashed)
|
||||||
{
|
{
|
||||||
<div class="card card-flush h-md-100">
|
<div class="card card-flush h-md-100">
|
||||||
<div class="card-body d-flex flex-column justify-content-between mt-9 bgi-no-repeat bgi-size-cover bgi-position-x-center pb-0">
|
<div class="card-body d-flex flex-column justify-content-between mt-9 bgi-no-repeat bgi-size-cover bgi-position-x-center pb-0">
|
||||||
@@ -30,6 +31,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
else if (SoftCrashed)
|
||||||
|
{
|
||||||
|
<div class="card card-body bg-danger mb-5">
|
||||||
|
<span class="text-center">
|
||||||
|
@(ErrorMessage)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@ChildContent
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ChildContent
|
@ChildContent
|
||||||
@@ -37,7 +48,22 @@ else
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private bool Crashed = false;
|
private bool HardCrashed = false;
|
||||||
|
private bool SoftCrashed = false;
|
||||||
|
private string ErrorMessage = "";
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
NavigationManager.LocationChanged += OnPathChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPathChanged(object? sender, LocationChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (SoftCrashed)
|
||||||
|
SoftCrashed = false;
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnErrorAsync(Exception exception)
|
protected override async Task OnErrorAsync(Exception exception)
|
||||||
{
|
{
|
||||||
@@ -50,74 +76,57 @@ else
|
|||||||
{
|
{
|
||||||
if (displayException.DoNotTranslate)
|
if (displayException.DoNotTranslate)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(displayException.Message);
|
||||||
displayException.Message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate(displayException.Message));
|
||||||
SmartTranslateService.Translate(displayException.Message)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (exception is CloudflareException cloudflareException)
|
else if (exception is CloudflareException cloudflareException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from cloudflare: ") + cloudflareException.Message);
|
||||||
SmartTranslateService.Translate("Error from cloudflare api"),
|
|
||||||
cloudflareException.Message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (exception is WingsException wingsException)
|
else if (exception is WingsException wingsException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from wings: ") + wingsException.Message);
|
||||||
SmartTranslateService.Translate("Error from wings"),
|
|
||||||
wingsException.Message
|
|
||||||
);
|
|
||||||
|
|
||||||
//TODO: Error log service
|
|
||||||
|
|
||||||
Logger.Warn($"Wings exception status code: {wingsException.StatusCode}");
|
Logger.Warn($"Wings exception status code: {wingsException.StatusCode}");
|
||||||
}
|
}
|
||||||
else if (exception is DaemonException daemonException)
|
else if (exception is DaemonException daemonException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from daemon: ") + daemonException.Message);
|
||||||
SmartTranslateService.Translate("Error from daemon"),
|
|
||||||
daemonException.Message
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.Warn($"Wings exception status code: {daemonException.StatusCode}");
|
Logger.Warn($"Wings exception status code: {daemonException.StatusCode}");
|
||||||
}
|
}
|
||||||
else if (exception is ModrinthException modrinthException)
|
else if (exception is ModrinthException modrinthException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from modrinth: ") + modrinthException.Message);
|
||||||
SmartTranslateService.Translate("Error from modrinth"),
|
|
||||||
modrinthException.Message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (exception is CloudPanelException cloudPanelException)
|
else if (exception is CloudPanelException cloudPanelException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from cloudpanel: ") + cloudPanelException.Message);
|
||||||
SmartTranslateService.Translate("Error from cloud panel"),
|
|
||||||
cloudPanelException.Message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (exception is NotImplementedException)
|
else if (exception is NotImplementedException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(SmartTranslateService.Translate("This function is not implemented"));
|
await SoftCrash(SmartTranslateService.Translate("This function is not implemented"));
|
||||||
}
|
}
|
||||||
else if (exception is StripeException stripeException)
|
else if (exception is StripeException stripeException)
|
||||||
{
|
{
|
||||||
await AlertService.Error(
|
await SoftCrash(SmartTranslateService.Translate("Error from stripe: ") + stripeException.Message);
|
||||||
SmartTranslateService.Translate("Unknown error from stripe"),
|
|
||||||
stripeException.Message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warn(exception);
|
Logger.Warn(exception);
|
||||||
Crashed = true;
|
HardCrashed = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task SoftCrash(string message)
|
||||||
|
{
|
||||||
|
SoftCrashed = true;
|
||||||
|
ErrorMessage = message;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,13 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<div class="card bg-black rounded">
|
<div class="card bg-black rounded">
|
||||||
|
@if (ShowHeader)
|
||||||
|
{
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">@(Header)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
|
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,6 +51,12 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public bool HideControls { get; set; } = false;
|
public bool HideControls { get; set; } = false;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowHeader { get; set; } = false;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Header { get; set; } = "Header.changeme.txt";
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Action<string> OnSubmit { get; set; }
|
public Action<string> OnSubmit { get; set; }
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
Language="@EditorLanguage"
|
Language="@EditorLanguage"
|
||||||
OnCancel="() => Cancel()"
|
OnCancel="() => Cancel()"
|
||||||
OnSubmit="(_) => Save()"
|
OnSubmit="(_) => Save()"
|
||||||
HideControls="false">
|
HideControls="false"
|
||||||
|
ShowHeader="true"
|
||||||
|
Header="@(EditingFile.Name)">
|
||||||
</FileEditor>
|
</FileEditor>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
<tbody class="fw-semibold text-gray-600">
|
<tbody class="fw-semibold text-gray-600">
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
<ContentBlock @ref="ContentBlock" AllowContentOverride="true">
|
<ContentBlock @ref="ContentBlock" AllowContentOverride="true">
|
||||||
|
@if (Access.CurrentPath != "/")
|
||||||
|
{
|
||||||
<tr class="even">
|
<tr class="even">
|
||||||
<td class="w-10px">
|
<td class="w-10px">
|
||||||
</td>
|
</td>
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
}
|
||||||
@foreach (var file in Data)
|
@foreach (var file in Data)
|
||||||
{
|
{
|
||||||
<tr class="even">
|
<tr class="even">
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="card mb-5 mb-xl-10">
|
||||||
|
<div class="card-body pt-0 pb-0">
|
||||||
|
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/domains">
|
||||||
|
<TL>Domains</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/domains/shared">
|
||||||
|
<TL>Shared domains</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Index { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
<TL>Logs</TL>
|
<TL>Logs</TL>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 5 ? "active" : "")" href="/admin/security/ddos">
|
||||||
|
<TL>Ddos protection</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="card mb-5 mb-xl-10">
|
||||||
|
<div class="card-body pt-0 pb-0">
|
||||||
|
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/statistics">
|
||||||
|
<TL>Overview</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/statistics/live">
|
||||||
|
<TL>Live data</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Index { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -34,6 +34,11 @@
|
|||||||
<TL>Mail</TL>
|
<TL>Mail</TL>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 10 ? "active" : "")" href="/admin/system/plugins">
|
||||||
|
<TL>Plugins</TL>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,13 +41,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="app-navbar flex-shrink-0">
|
<div class="app-navbar flex-shrink-0">
|
||||||
<div class="app-navbar-item ms-1 ms-lg-3">
|
|
||||||
<ThemeSwitcher
|
|
||||||
ToggleBtnClass="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"
|
|
||||||
ToggleBtnIconClass="svg-icon svg-icon-2">
|
|
||||||
</ThemeSwitcher>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (User != null)
|
@if (User != null)
|
||||||
{
|
{
|
||||||
<div class="app-navbar-item ms-1 ms-lg-3">
|
<div class="app-navbar-item ms-1 ms-lg-3">
|
||||||
@@ -56,43 +49,34 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-navbar-item ms-1 ms-lg-3" id="kt_header_user_menu_toggle">
|
<div class="app-navbar-item ms-1 ms-lg-3 dropdown">
|
||||||
<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">
|
<!-- Trigger -->
|
||||||
<img alt="Avatar" src="/api/moonlight/avatar/@(User.Id)"/>
|
<a class="cursor-pointer d-block symbol symbol-35px symbol-md-40px" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
</div>
|
<img alt="Avatar" src="/api/moonlight/avatar/@(User.Id)" width="35" height="35">
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg menu-state-primary fw-semibold py-4 fs-6 w-275px" data-kt-menu="true">
|
<!-- Dropdown Menu -->
|
||||||
<div class="menu-item px-3">
|
<div class="dropdown-menu dropdown-menu-end w-275px py-1" aria-labelledby="dropdownMenuLink">
|
||||||
<div class="menu-content d-flex align-items-center px-3">
|
<div class="dropdown-item py-4 bg-light bg-hover-light">
|
||||||
<div class="symbol symbol-50px me-5">
|
<div class="d-flex align-items-center">
|
||||||
<img alt="Avatar" src="/api/moonlight/avatar/@(User.Id)"/>
|
<img alt="Avatar" src="/api/moonlight/avatar/@(User.Id)" class="rounded-circle me-3" width="50" height="50">
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="fw-bold d-flex align-items-center fs-5">
|
<div class="fw-bold d-flex align-items-center">
|
||||||
<div class="@(User.StreamerMode ? "blur" : "")">
|
<div class="@(User.StreamerMode ? "blur" : "")">
|
||||||
@(User.FirstName) @(User.LastName)
|
@(User.FirstName) @(User.LastName)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (User.Admin)
|
@if (User.Admin)
|
||||||
{
|
{
|
||||||
<span class="badge badge-light-success fw-bold fs-8 px-2 py-1 ms-2">Admin</span>
|
<span class="badge bg-success fw-bold ms-2">Admin</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<a class="fw-semibold text-muted text-hover-primary fs-7 @(User.StreamerMode ? "blur" : "")">@(User.Email)</a>
|
<a class="fw-semibold text-muted text-decoration-none @(User.StreamerMode ? "blur" : "")">@User.Email</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator my-2"></div>
|
<hr class="dropdown-divider">
|
||||||
<div class="menu-item px-5 my-1">
|
<a href="/profile" class="dropdown-item py-4">Profile</a>
|
||||||
<a href="/profile" class="menu-link px-5">
|
<a href="#" @onclick:preventDefault @onclick="Logout" class="dropdown-item py-4">Logout</a>
|
||||||
<TL>Profile</TL>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item px-5">
|
|
||||||
<a @onclick="Logout" class="menu-link px-5">
|
|
||||||
<TL>Logout</TL>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
@using Moonlight.App.Services.Files
|
|
||||||
|
|
||||||
@inject ResourceService ResourceService
|
|
||||||
|
|
||||||
<div id="kt_app_header" class="app-header">
|
|
||||||
<div class="app-container container-fluid d-flex align-items-stretch justify-content-between">
|
|
||||||
<div class="d-flex align-items-center d-lg-none ms-n2 me-2" title="Show sidebar menu">
|
|
||||||
<div class="btn btn-icon btn-active-color-primary w-35px h-35px" id="kt_app_sidebar_mobile_toggle">
|
|
||||||
<i class="bx bx-menu bx-md"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center flex-grow-1 flex-lg-grow-0">
|
|
||||||
<a href="/" class="d-lg-none">
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-30px"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-stretch justify-content-between flex-lg-grow-1" id="kt_app_header_wrapper">
|
|
||||||
<div class="app-header-menu app-header-mobile-drawer align-items-stretch" data-kt-drawer="true" data-kt-drawer-name="app-header-menu" data-kt-drawer-activate="{default: true, lg: false}" data-kt-drawer-overlay="true" data-kt-drawer-width="225px" data-kt-drawer-direction="end" data-kt-drawer-toggle="#kt_app_header_menu_toggle" data-kt-swapper="true" data-kt-swapper-mode="{default: 'append', lg: 'prepend'}" data-kt-swapper-parent="{default: '#kt_app_body', lg: '#kt_app_header_wrapper'}">
|
|
||||||
<div class="menu menu-rounded menu-column menu-lg-row my-5 my-lg-0 align-items-stretch fw-semibold px-2 px-lg-0" id="kt_app_header_menu" data-kt-menu="true">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Navbar></Navbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
@using Moonlight.App.Services.Sessions
|
|
||||||
@using Moonlight.App.Database.Entities
|
|
||||||
@using Moonlight.App.Services
|
|
||||||
@using Moonlight.App.Services.Files
|
|
||||||
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
@inject ResourceService ResourceService
|
|
||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<div id="kt_app_sidebar" class="app-sidebar flex-column" data-kt-drawer="true" data-kt-drawer-name="app-sidebar" data-kt-drawer-activate="{default: true, lg: false}" data-kt-drawer-overlay="true" data-kt-drawer-width="225px" data-kt-drawer-direction="start" data-kt-drawer-toggle="#kt_app_sidebar_mobile_toggle">
|
|
||||||
<div class="app-sidebar-logo px-6" id="kt_app_sidebar_logo">
|
|
||||||
<a href="@(User != null ? "/" : "/login")">
|
|
||||||
@if (sidebar == "dark-sidebar")
|
|
||||||
{
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logolong.png"))" class="h-45px app-sidebar-logo-default"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (sidebar == "light-sidebar")
|
|
||||||
{
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="theme-light-show h-20px app-sidebar-logo-default"/>
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="theme-dark-show h-20px app-sidebar-logo-default"/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-20px app-sidebar-logo-minimize"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div id="kt_app_sidebar_toggle" class="app-sidebar-toggle btn btn-icon btn-shadow btn-sm btn-color-muted btn-active-color-primary body-bg h-30px w-30px position-absolute top-50 start-100 translate-middle rotate" data-kt-toggle="true" data-kt-toggle-state="active" data-kt-toggle-target="body" data-kt-toggle-name="app-sidebar-minimize">
|
|
||||||
<i class="bx bx-chevrons-left bx-md"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SidebarMenu></SidebarMenu>
|
|
||||||
|
|
||||||
<div class="app-sidebar-footer flex-column-auto pt-2 pb-6 px-6" id="kt_app_sidebar_footer">
|
|
||||||
<a href="/support" class="btn btn-flex flex-center btn-custom btn-primary overflow-hidden text-nowrap px-0 h-40px w-100 btn-label">
|
|
||||||
<i class="bx bx-sm bx-support"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private string sidebar;
|
|
||||||
|
|
||||||
private User? User;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
User = IdentityService.User;
|
|
||||||
sidebar = await JsRuntime.InvokeAsync<string>("document.body.getAttribute", "data-kt-app-layout");
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
@using Moonlight.App.Services
|
|
||||||
@using Moonlight.App.Services.Sessions
|
|
||||||
@using Moonlight.App.Database.Entities
|
|
||||||
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
|
|
||||||
<div class="app-sidebar-menu overflow-hidden flex-column-fluid">
|
|
||||||
<div id="kt_app_sidebar_menu_wrapper" class="app-sidebar-wrapper hover-scroll-overlay-y my-5" data-kt-scroll="true" data-kt-scroll-activate="true" data-kt-scroll-height="auto" data-kt-scroll-dependencies="#kt_app_sidebar_logo, #kt_app_sidebar_footer" data-kt-scroll-wrappers="#kt_app_sidebar_menu" data-kt-scroll-offset="5px" data-kt-scroll-save-state="true">
|
|
||||||
<div class="menu menu-column menu-rounded menu-sub-indention px-3" id="#kt_app_sidebar_menu" data-kt-menu="true" data-kt-menu-expand="false">
|
|
||||||
@if (User == null)
|
|
||||||
{
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/login">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bxs-log-in"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Login</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/register">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-user-plus"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Register</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-layer"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Dashboard</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/servers">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-server"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Servers</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/webspaces">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-globe"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Webspaces</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/domains">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-purchase-tag"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Domains</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/changelog">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-notepad"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Changelog</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
if (IdentityService.Permissions.HasAnyPermissions())
|
|
||||||
{
|
|
||||||
<div class="menu-item pt-5">
|
|
||||||
<div class="menu-content">
|
|
||||||
<span class="menu-heading fw-bold text-uppercase fs-7"><TL>Admin</TL></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-layer"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Dashboard</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/system">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-chip"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>System</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/security">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-shield"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Security</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/servers">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-server"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Servers</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/webspaces">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-globe"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Webspaces</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/users">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-user"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Users</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div data-kt-menu-trigger="click" class="menu-item menu-accordion">
|
|
||||||
<span class="menu-link">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-purchase-tag"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Domains</TL></span>
|
|
||||||
<span class="menu-arrow"></span>
|
|
||||||
</span>
|
|
||||||
<div class="menu-sub menu-sub-accordion">
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/domains/">
|
|
||||||
<span class="menu-bullet">
|
|
||||||
<span class="bullet bullet-dot"></span>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Domains</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/domains/shared">
|
|
||||||
<span class="menu-bullet">
|
|
||||||
<span class="bullet bullet-dot"></span>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Shared domains</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item">
|
|
||||||
<a class="menu-link" href="/admin/support">
|
|
||||||
<span class="menu-icon">
|
|
||||||
<i class="bx bx-support"></i>
|
|
||||||
</span>
|
|
||||||
<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">
|
|
||||||
<i class="bx bx-objects-vertical-bottom"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title"><TL>Statistics</TL></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private User? User;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
User = IdentityService.User;
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<a href="#" class="@ToggleBtnClass" data-kt-menu-trigger="@Trigger" data-kt-menu-attach="parent" data-kt-menu-placement="@MenuPlacement">
|
|
||||||
<i class="theme-light-show bx bx-sun"></i>
|
|
||||||
<i class="theme-dark-show bx bx-moon" ></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-muted menu-active-bg menu-state-primary fw-semibold py-4 fs-base w-175px" data-kt-menu="true" data-kt-element="theme-mode-menu">
|
|
||||||
<div class="menu-item px-3 my-0">
|
|
||||||
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="light" @onclick="TriggerFlashbang">
|
|
||||||
<span class="menu-icon" data-kt-element="icon">
|
|
||||||
<i class="bx bx-sun"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title">Lightmode</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item px-3 my-0">
|
|
||||||
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="dark">
|
|
||||||
<span class="menu-icon" data-kt-element="icon">
|
|
||||||
<i class="bx bx-moon"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title">Darkmode</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item px-3 my-0">
|
|
||||||
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="system">
|
|
||||||
<span class="menu-icon" data-kt-element="icon">
|
|
||||||
<i class="bx bx-cog"></i>
|
|
||||||
</span>
|
|
||||||
<span class="menu-title">System</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public string ToggleBtnClass { get; set; } = "";
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string MenuPlacement { get; set; } = "bottom-end";
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string ToggleBtnIconClass { get; set; } = "svg-icon svg-icon-2";
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string Trigger { get; set; } = "{default:'click'}";
|
|
||||||
|
|
||||||
private async void TriggerFlashbang()
|
|
||||||
{
|
|
||||||
await JsRuntime.InvokeVoidAsync("moonlight.flashbang.run");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
236
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
236
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Files
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
|
||||||
|
@inject ResourceService ResourceService
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
|
<div class="scroll-y me-n5 pe-5" style="max-height: 50vh; display: flex; flex-direction: column-reverse;">
|
||||||
|
@foreach (var message in Messages.OrderByDescending(x => x.Id)) // Reverse messages to use auto scrolling
|
||||||
|
{
|
||||||
|
if (message.IsSupportMessage)
|
||||||
|
{
|
||||||
|
if (ViewAsSupport)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-end mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-end">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="me-3">
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<span class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-start mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<span class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.IsSystemMessage)
|
||||||
|
{
|
||||||
|
<div class="separator separator-content border-primary my-15">
|
||||||
|
<span class="w-250px fw-bold">
|
||||||
|
@(message.Content)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ViewAsSupport)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-start mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<span class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-end mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-end">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="me-3">
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
<span class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||||
|
</div>
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<TicketMessage> Messages { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ViewAsSupport { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Plugin.UI.Webspaces
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Moonlight.App.Services.Interop
|
@using Moonlight.App.Services.Interop
|
||||||
|
|
||||||
@@ -34,26 +35,14 @@
|
|||||||
<div class="card mb-xl-10 mb-5">
|
<div class="card mb-xl-10 mb-5">
|
||||||
<div class="card-body pt-0 pb-0">
|
<div class="card-body pt-0 pb-0">
|
||||||
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||||
|
@foreach (var tab in Context.Tabs)
|
||||||
|
{
|
||||||
<li class="nav-item mt-2">
|
<li class="nav-item mt-2">
|
||||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/webspace/@(WebSpace.Id)">
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Route == tab.Route ? "active" : "")" href="/webspace/@(WebSpace.Id + tab.Route)">
|
||||||
<TL>Dashboard</TL>
|
<TL>@(tab.Name)</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="/webspace/@(WebSpace.Id)/files">
|
|
||||||
<TL>Files</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="/webspace/@(WebSpace.Id)/sftp">
|
|
||||||
<TL>Sftp</TL>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item mt-2">
|
|
||||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/webspace/@(WebSpace.Id)/databases">
|
|
||||||
<TL>Databases</TL>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,11 +50,14 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int Index { get; set; }
|
public string Route { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public WebSpace WebSpace { get; set; }
|
public WebSpace WebSpace { get; set; }
|
||||||
|
|
||||||
|
[CascadingParameter]
|
||||||
|
public WebspacePageContext Context { get; set; }
|
||||||
|
|
||||||
private async Task Delete()
|
private async Task Delete()
|
||||||
{
|
{
|
||||||
if (await AlertService.ConfirmMath())
|
if (await AlertService.ConfirmMath())
|
||||||
|
|||||||
284
Moonlight/Shared/Layouts/DefaultLayout.razor
Normal file
284
Moonlight/Shared/Layouts/DefaultLayout.razor
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
@using Moonlight.App.Services.Files
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
|
||||||
|
@inject ResourceService ResourceService
|
||||||
|
@inject DynamicBackgroundService DynamicBackgroundService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
|
||||||
|
<div class="d-flex flex-column flex-root app-root">
|
||||||
|
<div class="app-page flex-column flex-column-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
|
||||||
|
<div class="app-header">
|
||||||
|
<div class="app-container container-fluid d-flex align-items-stretch justify-content-between">
|
||||||
|
<div class="d-flex align-items-center d-lg-none ms-n2 me-2" title="Show sidebar menu">
|
||||||
|
<a class="btn btn-icon btn-active-color-primary w-35px h-35px" @onclick:preventDefault @onclick="ToggleMobileSidebar">
|
||||||
|
<i class="bx bx-menu bx-md"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (ShowMobileSidebar)
|
||||||
|
{
|
||||||
|
<div style="z-index: 105;" class="drawer-overlay" @onclick="ToggleMobileSidebar"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center flex-grow-1 flex-lg-grow-0">
|
||||||
|
<a href="/" class="d-lg-none">
|
||||||
|
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-30px"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-stretch justify-content-between flex-lg-grow-1">
|
||||||
|
<div class="app-header-menu app-header-mobile-drawer align-items-stretch">
|
||||||
|
<div class="menu menu-rounded menu-column menu-lg-row my-5 my-lg-0 align-items-stretch fw-semibold px-2 px-lg-0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Navbar></Navbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Header End --->
|
||||||
|
<div class="app-wrapper flex-column flex-row-fluid">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
|
||||||
|
<div class="app-sidebar flex-column @(ShowMobileSidebar ? "drawer drawer-start drawer-on" : "")">
|
||||||
|
<div class="app-sidebar-logo px-6">
|
||||||
|
<a href="@(IdentityService.User != null ? "/" : "/login")">
|
||||||
|
<img alt="Logo" src="@(ResourceService.Image("logolong.png"))" class="h-45px app-sidebar-logo-default"/>
|
||||||
|
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-20px app-sidebar-logo-minimize"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-sidebar-menu overflow-hidden flex-column-fluid">
|
||||||
|
<div class="app-sidebar-wrapper hover-scroll-overlay-y my-5">
|
||||||
|
<div class="menu menu-column menu-rounded menu-sub-indention px-3">
|
||||||
|
@if (IdentityService.User == null)
|
||||||
|
{
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/login">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bxs-log-in"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Login</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/register">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-user-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Register</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-layer"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Dashboard</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/servers">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-server"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Servers</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/webspaces">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-globe"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Webspaces</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/domains">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-purchase-tag"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Domains</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if (IdentityService.Permissions.HasAnyPermissions())
|
||||||
|
{
|
||||||
|
<div class="menu-item pt-5">
|
||||||
|
<div class="menu-content">
|
||||||
|
<span class="menu-heading fw-bold text-uppercase fs-7">
|
||||||
|
<TL>Admin</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-layer"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Dashboard</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/system">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-chip"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>System</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/security">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-shield"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Security</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/servers">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-server"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Servers</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/webspaces">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-globe"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Webspaces</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/users">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-user"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Users</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/domains">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-purchase-tag"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Domains</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/support">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-support"></i>
|
||||||
|
</span>
|
||||||
|
<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">
|
||||||
|
<i class="bx bx-objects-vertical-bottom"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Statistics</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link" href="/admin/changelog">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-notepad"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
<TL>Changelog</TL>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-sidebar-footer flex-column-auto pt-2 pb-6 px-6">
|
||||||
|
<a href="/support" class="btn btn-flex flex-center btn-custom btn-primary overflow-hidden text-nowrap px-0 h-40px w-100 btn-label">
|
||||||
|
<i class="bx bx-sm bx-support"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar End -->
|
||||||
|
|
||||||
|
<div class="app-main flex-column flex-row-fluid">
|
||||||
|
<div class="d-flex flex-column flex-column-fluid">
|
||||||
|
<div class="app-content flex-column-fluid" style="background-position: center; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-image: linear-gradient(rgba(0, 0, 0, 0.55),rgba(0, 0, 0, 0.55)) ,url('@(DynamicBackgroundService.BackgroundImageUrl)');">
|
||||||
|
<div class="app-container container-fluid">
|
||||||
|
<div class="mt-10">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; }
|
||||||
|
|
||||||
|
private bool ShowMobileSidebar = false;
|
||||||
|
|
||||||
|
private async Task ToggleMobileSidebar()
|
||||||
|
{
|
||||||
|
ShowMobileSidebar = !ShowMobileSidebar;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
@using Moonlight.App.Services.Sessions
|
@using Moonlight.App.Services.Sessions
|
||||||
@using Moonlight.App.Events
|
@using Moonlight.App.Events
|
||||||
|
|
||||||
@layout ThemeInit
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
@@ -22,6 +21,8 @@
|
|||||||
@inject IpBanService IpBanService
|
@inject IpBanService IpBanService
|
||||||
@inject DynamicBackgroundService DynamicBackgroundService
|
@inject DynamicBackgroundService DynamicBackgroundService
|
||||||
@inject KeyListenerService KeyListenerService
|
@inject KeyListenerService KeyListenerService
|
||||||
|
@inject ConfigService ConfigService
|
||||||
|
@inject IpVerificationService IpVerificationService
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var uri = new Uri(NavigationManager.Uri);
|
var uri = new Uri(NavigationManager.Uri);
|
||||||
@@ -44,26 +45,37 @@
|
|||||||
<GlobalErrorBoundary>
|
<GlobalErrorBoundary>
|
||||||
<PageTitle>@(string.IsNullOrEmpty(title) ? "Dashboard - " : title)Moonlight</PageTitle>
|
<PageTitle>@(string.IsNullOrEmpty(title) ? "Dashboard - " : title)Moonlight</PageTitle>
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-root app-root" id="kt_app_root">
|
<DefaultLayout>
|
||||||
<div class="app-page flex-column flex-column-fluid" id="kt_app_page">
|
|
||||||
<canvas id="snow" class="snow-canvas"></canvas>
|
|
||||||
|
|
||||||
@{
|
|
||||||
//TODO: Add a way to disable the snow
|
|
||||||
}
|
|
||||||
|
|
||||||
<PageHeader></PageHeader>
|
|
||||||
<div class="app-wrapper flex-column flex-row-fluid" id="kt_app_wrapper">
|
|
||||||
<Sidebar></Sidebar>
|
|
||||||
<div class="app-main flex-column flex-row-fluid" id="kt_app_main">
|
|
||||||
<div class="d-flex flex-column flex-column-fluid">
|
|
||||||
<div id="kt_app_content" class="app-content flex-column-fluid" style="background-position: center; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-image: url('@(DynamicBackgroundService.BackgroundImageUrl)')">
|
|
||||||
<div id="kt_app_content_container" class="app-container container-fluid">
|
|
||||||
<div class="mt-10">
|
|
||||||
<SoftErrorBoundary>
|
<SoftErrorBoundary>
|
||||||
@if (!IsIpBanned)
|
@if (UserProcessed)
|
||||||
{
|
{
|
||||||
if (UserProcessed)
|
if (IsIpBanned)
|
||||||
|
{
|
||||||
|
<div class="modal d-block">
|
||||||
|
<div class="modal-dialog modal-dialog-centered mw-900px">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
||||||
|
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
|
||||||
|
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (IsIpSuspicious)
|
||||||
|
{
|
||||||
|
<div class="modal d-block">
|
||||||
|
<div class="modal-dialog modal-dialog-centered mw-900px">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
||||||
|
<h2>@(SmartTranslateService.Translate("Your ip his blocked. VPNs and Datacenter IPs are prohibited from accessing this site"))</h2>
|
||||||
|
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Please disable your vpn or proxy and try it again"))</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (uri.LocalPath != "/login" &&
|
if (uri.LocalPath != "/login" &&
|
||||||
uri.LocalPath != "/passwordreset" &&
|
uri.LocalPath != "/passwordreset" &&
|
||||||
@@ -117,6 +129,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="modal d-block">
|
<div class="modal d-block">
|
||||||
@@ -130,57 +143,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="modal d-block">
|
|
||||||
<div class="modal-dialog modal-dialog-centered mw-900px">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
|
||||||
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
|
|
||||||
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</SoftErrorBoundary>
|
</SoftErrorBoundary>
|
||||||
</div>
|
</DefaultLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Footer></Footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GlobalErrorBoundary>
|
</GlobalErrorBoundary>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private bool UserProcessed = false;
|
private bool UserProcessed = false;
|
||||||
|
|
||||||
private bool IsIpBanned = false;
|
private bool IsIpBanned = false;
|
||||||
|
private bool IsIpSuspicious = false;
|
||||||
protected override void OnAfterRender(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
AddBodyAttribute("data-kt-app-page-loading", "on");
|
|
||||||
|
|
||||||
//Initialize classes and attributes for layout with dark sidebar
|
|
||||||
AddBodyAttribute("data-kt-app-reset-transition", "true");
|
|
||||||
|
|
||||||
AddBodyAttribute("data-kt-app-layout", "dark-sidebar");
|
|
||||||
AddBodyAttribute("data-kt-app-header-fixed", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-sidebar-fixed", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-sidebar-hoverable", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-sidebar-push-header", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-sidebar-push-toolbar", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-sidebar-push-footer", "true");
|
|
||||||
AddBodyAttribute("data-kt-app-toolbar-enabled", "true");
|
|
||||||
|
|
||||||
AddBodyClass("app-default");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -190,11 +161,6 @@
|
|||||||
{
|
{
|
||||||
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
|
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
|
||||||
|
|
||||||
IsIpBanned = await IpBanService.IsBanned();
|
|
||||||
|
|
||||||
if (IsIpBanned)
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Event.On<Object>("ipBan.update", this, async _ =>
|
await Event.On<Object>("ipBan.update", this, async _ =>
|
||||||
{
|
{
|
||||||
IsIpBanned = await IpBanService.IsBanned();
|
IsIpBanned = await IpBanService.IsBanned();
|
||||||
@@ -202,21 +168,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
await IdentityService.Load();
|
await IdentityService.Load();
|
||||||
|
|
||||||
|
IsIpBanned = await IpBanService.IsBanned();
|
||||||
|
IsIpSuspicious = await IpVerificationService.IsDatacenterOrVpn(IdentityService.Ip);
|
||||||
|
|
||||||
UserProcessed = true;
|
UserProcessed = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-reset-transition");
|
|
||||||
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
|
|
||||||
await JsRuntime.InvokeVoidAsync("KTMenu.createInstances");
|
|
||||||
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
/* ignore errors to make sure that the session call is executed */
|
|
||||||
}
|
|
||||||
|
|
||||||
await SessionClientService.Start();
|
await SessionClientService.Start();
|
||||||
|
|
||||||
NavigationManager.LocationChanged += async (_, _) =>
|
NavigationManager.LocationChanged += async (_, _) =>
|
||||||
@@ -241,10 +199,12 @@
|
|||||||
|
|
||||||
await KeyListenerService.Initialize();
|
await KeyListenerService.Initialize();
|
||||||
|
|
||||||
RunDelayedMenu(0);
|
if (ConfigService.Get().Moonlight.EnableLatencyCheck)
|
||||||
RunDelayedMenu(1);
|
{
|
||||||
RunDelayedMenu(3);
|
await JsRuntime.InvokeVoidAsync("moonlight.loading.checkConnection",
|
||||||
RunDelayedMenu(5);
|
ConfigService.Get().Moonlight.AppUrl,
|
||||||
|
ConfigService.Get().Moonlight.LatencyCheckThreshold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -264,31 +224,4 @@
|
|||||||
await Event.Off($"supportChat.{IdentityService.User.Id}.message", this);
|
await Event.Off($"supportChat.{IdentityService.User.Id}.message", this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddBodyAttribute(string attribute, string value)
|
|
||||||
{
|
|
||||||
JsRuntime.InvokeVoidAsync("document.body.setAttribute", attribute, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddBodyClass(string className)
|
|
||||||
{
|
|
||||||
JsRuntime.InvokeVoidAsync("document.body.classList.add", className);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunDelayedMenu(int seconds)
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(seconds));
|
|
||||||
await JsRuntime.InvokeVoidAsync("KTMenu.initHandlers");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
//Logger.Warn("Delayed menu error");
|
|
||||||
//Logger.Warn(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
@using Moonlight.App.Extensions
|
@using Moonlight.App.Extensions
|
||||||
@using Moonlight.App.Services.Sessions
|
@using Moonlight.App.Services.Sessions
|
||||||
|
|
||||||
@layout ThemeInit
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
@using Moonlight.App.Extensions
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
@inject IJSRuntime JS
|
|
||||||
@inject NavigationManager NavigationManager
|
|
||||||
|
|
||||||
@Body
|
|
||||||
|
|
||||||
<script suppress-error="BL9992">
|
|
||||||
window.emptyBody = function(){
|
|
||||||
document.body.className = '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
|
||||||
{
|
|
||||||
JS.InvokeVoidSafe("KTThemeMode.init");
|
|
||||||
JS.InvokeVoidSafe("emptyBody");
|
|
||||||
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
JS.InvokeVoidSafe("scrollTo", 0, 0);
|
|
||||||
JS.InvokeVoidSafe("KTDialer.init");
|
|
||||||
JS.InvokeVoidSafe("KTDrawer.init");
|
|
||||||
JS.InvokeVoidSafe("KTMenu.init");
|
|
||||||
JS.InvokeVoidSafe("KTImageInput.init");
|
|
||||||
JS.InvokeVoidSafe("KTPasswordMeter.init");
|
|
||||||
JS.InvokeVoidSafe("KTScroll.init");
|
|
||||||
JS.InvokeVoidSafe("KTScrolltop.init");
|
|
||||||
JS.InvokeVoidSafe("KTSticky.init");
|
|
||||||
JS.InvokeVoidSafe("KTSwapper.init");
|
|
||||||
JS.InvokeVoidSafe("KTToggle.init");
|
|
||||||
JS.InvokeVoidSafe("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(NavigationManager.Uri)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
JS.InvokeVoidAsync("KTLayoutSearch.init");
|
|
||||||
JS.InvokeVoidAsync("KTAppSidebar.init");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
NavigationManager.LocationChanged += OnLocationChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnLocationChanged(object sender, LocationChangedEventArgs args)
|
|
||||||
{
|
|
||||||
await JS.InvokeVoidSafeAsync("scrollTo", 0, 0);
|
|
||||||
await JS.InvokeVoidSafeAsync("KTDrawer.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTMenu.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTImageInput.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTPasswordMeter.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTScroll.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTScrolltop.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTSticky.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTSwapper.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTToggle.createInstances");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(args.Location)}");
|
|
||||||
await JS.InvokeVoidSafeAsync("KTAppSidebar.init");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
@page "/changelog"
|
@page "/admin/changelog"
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
|
|
||||||
@inject MoonlightService MoonlightService
|
@inject MoonlightService MoonlightService
|
||||||
|
|
||||||
|
@attribute [PermissionRequired(nameof(Permissions.AdminChangelog))]
|
||||||
|
|
||||||
@{
|
@{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using BlazorTable
|
@using BlazorTable
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.Shared.Components.Navigations
|
||||||
|
|
||||||
@inject DomainRepository DomainRepository
|
@inject DomainRepository DomainRepository
|
||||||
@inject DomainService DomainService
|
@inject DomainService DomainService
|
||||||
@@ -11,8 +12,9 @@
|
|||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminDomains))]
|
@attribute [PermissionRequired(nameof(Permissions.AdminDomains))]
|
||||||
|
|
||||||
|
<AdminDomainsNavigation Index="0" />
|
||||||
|
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||||
<div class="row">
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header border-0 pt-5">
|
<div class="card-header border-0 pt-5">
|
||||||
<h3 class="card-title align-items-start flex-column">
|
<h3 class="card-title align-items-start flex-column">
|
||||||
@@ -60,7 +62,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Services.Interop
|
@using Moonlight.App.Services.Interop
|
||||||
@using BlazorTable
|
@using BlazorTable
|
||||||
|
@using Moonlight.Shared.Components.Navigations
|
||||||
|
|
||||||
@inject SharedDomainRepository SharedDomainRepository
|
@inject SharedDomainRepository SharedDomainRepository
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
@@ -14,18 +15,24 @@
|
|||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminSharedDomains))]
|
@attribute [PermissionRequired(nameof(Permissions.AdminSharedDomains))]
|
||||||
|
|
||||||
|
<AdminDomainsNavigation Index="1" />
|
||||||
|
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header border-0 pt-5">
|
<div class="card-header border-0 pt-5">
|
||||||
<h3 class="card-title align-items-start flex-column">
|
<h3 class="card-title align-items-start flex-column">
|
||||||
<span class="card-label fw-bold fs-3 mb-1">
|
<span class="card-label fw-bold fs-3 mb-1">
|
||||||
<span><TL>Shared domains</TL></span>
|
<span>
|
||||||
|
<TL>Shared domains</TL>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="card-toolbar">
|
<div class="card-toolbar">
|
||||||
<a href="/admin/domains/shared/new" class="btn btn-sm btn-light-success">
|
<a href="/admin/domains/shared/new" class="btn btn-sm btn-light-success">
|
||||||
<i class="bx bx-layer-plus"></i>
|
<i class="bx bx-layer-plus"></i>
|
||||||
<span><TL>Add shared domain</TL></span>
|
<span>
|
||||||
|
<TL>Add shared domain</TL>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +51,7 @@
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
@page "/admin/nodes/ddos"
|
|
||||||
|
|
||||||
@using Moonlight.Shared.Components.Navigations
|
|
||||||
@using Moonlight.App.Repositories
|
|
||||||
@using BlazorTable
|
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using Moonlight.App.Database.Entities
|
|
||||||
@using Moonlight.App.Events
|
|
||||||
@using Moonlight.App.Helpers
|
|
||||||
@using Moonlight.App.Services
|
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
@inject DdosAttackRepository DdosAttackRepository
|
|
||||||
@inject SmartTranslateService SmartTranslateService
|
|
||||||
@inject EventSystem Event
|
|
||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminNodeDdos))]
|
|
||||||
|
|
||||||
<AdminNodesNavigation Index="1"/>
|
|
||||||
|
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body pt-0">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<Table TableItem="DdosAttack" Items="DdosAttacks" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
|
||||||
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
|
||||||
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
|
|
||||||
<Template>
|
|
||||||
@if (context.Ongoing)
|
|
||||||
{
|
|
||||||
<TL>DDos attack started</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<TL>DDos attack stopped</TL>
|
|
||||||
}
|
|
||||||
</Template>
|
|
||||||
</Column>
|
|
||||||
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Node"))" Field="@(x => x.Node)" Sortable="false" Filterable="false">
|
|
||||||
<Template>
|
|
||||||
<a href="/admin/nodes/view/@(context.Id)">
|
|
||||||
@(context.Node.Name)
|
|
||||||
</a>
|
|
||||||
</Template>
|
|
||||||
</Column>
|
|
||||||
<Column TableItem="DdosAttack" Title="Ip" Field="@(x => x.Ip)" Sortable="true" Filterable="true"/>
|
|
||||||
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
|
|
||||||
<Template>
|
|
||||||
@if (context.Ongoing)
|
|
||||||
{
|
|
||||||
@(context.Data)
|
|
||||||
<TL> packets</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@(context.Data)
|
|
||||||
<span> MB</span>
|
|
||||||
}
|
|
||||||
</Template>
|
|
||||||
</Column>
|
|
||||||
<Column TableItem="DdosAttack" Title="@(SmartTranslateService.Translate("Date"))" Field="@(x => x.Ongoing)" Sortable="true" Filterable="true">
|
|
||||||
<Template>
|
|
||||||
@(Formatter.FormatDate(context.CreatedAt))
|
|
||||||
</Template>
|
|
||||||
</Column>
|
|
||||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LazyLoader>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private DdosAttack[] DdosAttacks;
|
|
||||||
private LazyLoader LazyLoader;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
await Event.On<DdosAttack>("node.ddos", this, async attack => await LazyLoader.Reload());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task Load(LazyLoader arg)
|
|
||||||
{
|
|
||||||
DdosAttacks = DdosAttackRepository
|
|
||||||
.Get()
|
|
||||||
.Include(x => x.Node)
|
|
||||||
.OrderByDescending(x => x.CreatedAt)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Dispose()
|
|
||||||
{
|
|
||||||
await Event.Off("node.ddos", this);
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Move to security
|
|
||||||
}
|
|
||||||
@@ -88,7 +88,7 @@ else
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>
|
<span>
|
||||||
@(Formatter.FormatSize(MemoryMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(MemoryMetrics.Total)) <TL>memory used</TL>
|
@(Formatter.FormatSize(ByteSizeValue.FromKiloBytes(MemoryMetrics.Used).Bytes)) <TL>of</TL> @(Formatter.FormatSize(ByteSizeValue.FromKiloBytes(MemoryMetrics.Total).Bytes)) <TL>memory used</TL>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
148
Moonlight/Shared/Views/Admin/Security/Ddos.razor
Normal file
148
Moonlight/Shared/Views/Admin/Security/Ddos.razor
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
@page "/admin/security/ddos"
|
||||||
|
|
||||||
|
@using Moonlight.Shared.Components.Navigations
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using BlazorTable
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Services.Background
|
||||||
|
|
||||||
|
@inject Repository<BlocklistIp> BlocklistIpRepository
|
||||||
|
@inject Repository<WhitelistIp> WhitelistIpRepository
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject DdosProtectionService DdosProtectionService
|
||||||
|
|
||||||
|
@attribute [PermissionRequired(nameof(Permissions.AdminSecurityDdos))]
|
||||||
|
|
||||||
|
<AdminSecurityNavigation Index="5"/>
|
||||||
|
|
||||||
|
<div class="card card-body mb-5">
|
||||||
|
<div class="d-flex justify-content-center fs-4">
|
||||||
|
<span class="me-3">
|
||||||
|
@(BlocklistIps.Length) <TL>blocked IPs</TL>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
@(WhitelistIps.Length) <TL>whitelisted IPs</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-body mb-5">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<input @bind="Ip" type="text" class="form-control">
|
||||||
|
<WButton CssClasses="btn-secondary" OnClick="BlockIp" Text="@(SmartTranslateService.Translate("Block"))"/>
|
||||||
|
<WButton CssClasses="btn-secondary" OnClick="WhitelistIp" Text="@(SmartTranslateService.Translate("Whitelist"))"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-body mb-5">
|
||||||
|
<LazyLoader @ref="BlocklistLazyLoader" Load="LoadBlocklist">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<Table TableItem="BlocklistIp" Items="BlocklistIps" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="BlocklistIp" Width="30%" Title="@(SmartTranslateService.Translate("Ip"))" Field="@(x => x.Ip)" Filterable="true" Sortable="false"/>
|
||||||
|
<Column TableItem="BlocklistIp" Width="30%" Title="@(SmartTranslateService.Translate("Packets"))" Field="@(x => x.Packets)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@(context.Packets) <TL>packets</TL>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="BlocklistIp" Width="30%" Title="" Field="@(x => x.ExpiresAt)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@Formatter.FormatUptime(context.ExpiresAt - DateTime.UtcNow) <TL>remaining</TL>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="BlocklistIp" Width="15%" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<div class="text-end">
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Details"))" />
|
||||||
|
<DeleteButton Confirm="true" OnClick="() => RevokeBlocklistIp(context)" />
|
||||||
|
</div>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-body">
|
||||||
|
<LazyLoader @ref="WhitelistLazyLoader" Load="LoadWhitelist">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<Table TableItem="WhitelistIp" Items="WhitelistIps" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="WhitelistIp" Width="85%" Title="@(SmartTranslateService.Translate("Ip"))" Field="@(x => x.Ip)" Filterable="true" Sortable="false"/>
|
||||||
|
<Column TableItem="WhitelistIp" Width="15%" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<div class="text-end">
|
||||||
|
<DeleteButton Confirm="true" OnClick="() => RevokeWhitelistIp(context)" />
|
||||||
|
</div>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private BlocklistIp[] BlocklistIps = Array.Empty<BlocklistIp>();
|
||||||
|
private WhitelistIp[] WhitelistIps = Array.Empty<WhitelistIp>();
|
||||||
|
|
||||||
|
private LazyLoader BlocklistLazyLoader;
|
||||||
|
private LazyLoader WhitelistLazyLoader;
|
||||||
|
|
||||||
|
private string Ip = "";
|
||||||
|
|
||||||
|
private async Task LoadBlocklist(LazyLoader _)
|
||||||
|
{
|
||||||
|
BlocklistIps = BlocklistIpRepository
|
||||||
|
.Get()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadWhitelist(LazyLoader _)
|
||||||
|
{
|
||||||
|
WhitelistIps = WhitelistIpRepository
|
||||||
|
.Get()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BlockIp()
|
||||||
|
{
|
||||||
|
await DdosProtectionService.BlocklistIp(Ip, -1);
|
||||||
|
|
||||||
|
Ip = "";
|
||||||
|
|
||||||
|
await BlocklistLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevokeBlocklistIp(BlocklistIp blocklistIp)
|
||||||
|
{
|
||||||
|
await DdosProtectionService.UnBlocklistIp(blocklistIp.Ip);
|
||||||
|
|
||||||
|
await BlocklistLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhitelistIp()
|
||||||
|
{
|
||||||
|
WhitelistIpRepository.Add(new()
|
||||||
|
{
|
||||||
|
Ip = Ip
|
||||||
|
});
|
||||||
|
|
||||||
|
Ip = "";
|
||||||
|
|
||||||
|
await WhitelistLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevokeWhitelistIp(WhitelistIp whitelistIp)
|
||||||
|
{
|
||||||
|
WhitelistIpRepository.Delete(whitelistIp);
|
||||||
|
|
||||||
|
await WhitelistLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@
|
|||||||
{
|
{
|
||||||
SecurityLogs = SecurityLogRepository
|
SecurityLogs = SecurityLogRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.ToArray()
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|||||||
@@ -4,12 +4,23 @@
|
|||||||
@using Moonlight.App.Services.Background
|
@using Moonlight.App.Services.Background
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using BlazorTable
|
@using BlazorTable
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.ApiClients.Wings
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Events
|
@using Moonlight.App.Events
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Models.Misc
|
@using Moonlight.App.Models.Misc
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Services.Interop
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
|
||||||
@inject MalwareScanService MalwareScanService
|
@inject MalwareBackgroundScanService MalwareBackgroundScanService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject ServerService ServerService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject SessionServerService SessionServerService
|
||||||
|
@inject Repository<Server> ServerRepository
|
||||||
|
@inject Repository<User> UserRepository
|
||||||
@inject EventSystem Event
|
@inject EventSystem Event
|
||||||
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@@ -22,15 +33,15 @@
|
|||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (MalwareScanService.IsRunning)
|
@if (MalwareBackgroundScanService.IsRunning)
|
||||||
{
|
{
|
||||||
<span class="fs-3 spinner-border align-middle me-3"></span>
|
<span class="fs-3 spinner-border align-middle me-3"></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<span class="fs-3">@(MalwareScanService.Status)</span>
|
<span class="fs-3">@(MalwareBackgroundScanService.Status)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
@if (MalwareScanService.IsRunning)
|
@if (MalwareBackgroundScanService.IsRunning)
|
||||||
{
|
{
|
||||||
<button class="btn btn-success disabled">
|
<button class="btn btn-success disabled">
|
||||||
<TL>Scan in progress</TL>
|
<TL>Scan in progress</TL>
|
||||||
@@ -38,9 +49,23 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareBackgroundScanService.ScanAllServers">
|
||||||
|
<label class="form-check-label" for="scanAllServers">
|
||||||
|
<TL>Scan all servers</TL>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
|
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
|
||||||
CssClasses="btn-success"
|
CssClasses="btn-success me-3"
|
||||||
OnClick="MalwareScanService.Start">
|
OnClick="Scan">
|
||||||
|
</WButton>
|
||||||
|
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Purge page"))"
|
||||||
|
CssClasses="btn-danger"
|
||||||
|
OnClick="PurgeSelected">
|
||||||
</WButton>
|
</WButton>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -56,39 +81,14 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
|
<LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<Table TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
<Table @ref="Table" TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
<Template>
|
<Template>
|
||||||
<a href="/server/@(context.Uuid)">@(context.Name)</a>
|
<a href="/server/@(context.Uuid)">@(context.Name)</a>
|
||||||
</Template>
|
</Template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Results"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Title"))" Field="@(x => ScanResults[x].Title)" Sortable="false" Filterable="true" />
|
||||||
<Template>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => ScanResults[x].Description)" Sortable="false" Filterable="true" />
|
||||||
<div class="row">
|
|
||||||
@foreach (var result in ScanResults[context])
|
|
||||||
{
|
|
||||||
<div class="col-12 col-md-6 p-3">
|
|
||||||
<div class="accordion" id="scanResult@(result.GetHashCode())">
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="scanResult-header@(result.GetHashCode())">
|
|
||||||
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#scanResult-body@(result.GetHashCode())" aria-expanded="false" aria-controls="scanResult-body@(result.GetHashCode())">
|
|
||||||
<span>@(result.Title)</span>
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="scanResult-body@(result.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="scanResult-header@(result.GetHashCode())" data-bs-parent="#scanResult">
|
|
||||||
<div class="accordion-body">
|
|
||||||
<p>
|
|
||||||
@(result.Description)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Template>
|
|
||||||
</Column>
|
|
||||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,9 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new();
|
private readonly Dictionary<Server, MalwareScanResult> ScanResults = new();
|
||||||
|
|
||||||
|
private Table<Server> Table;
|
||||||
|
|
||||||
private LazyLoader LazyLoaderResults;
|
private LazyLoader LazyLoaderResults;
|
||||||
|
|
||||||
@@ -108,16 +110,27 @@
|
|||||||
{
|
{
|
||||||
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
|
await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
|
||||||
|
|
||||||
await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); });
|
await Event.On<Server?>("malwareScan.result", this, async server =>
|
||||||
|
{
|
||||||
|
lock (MalwareBackgroundScanService.ScanResults)
|
||||||
|
{
|
||||||
|
if (server == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ScanResults.Add(server, MalwareBackgroundScanService.ScanResults[server]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task LoadResults(LazyLoader arg)
|
private Task LoadResults(LazyLoader arg)
|
||||||
{
|
{
|
||||||
ScanResults.Clear();
|
ScanResults.Clear();
|
||||||
|
|
||||||
lock (MalwareScanService.ScanResults)
|
lock (MalwareBackgroundScanService.ScanResults)
|
||||||
{
|
{
|
||||||
foreach (var result in MalwareScanService.ScanResults)
|
foreach (var result in MalwareBackgroundScanService.ScanResults)
|
||||||
{
|
{
|
||||||
ScanResults.Add(result.Key, result.Value);
|
ScanResults.Add(result.Key, result.Value);
|
||||||
}
|
}
|
||||||
@@ -131,4 +144,76 @@
|
|||||||
await Event.Off("malwareScan.status", this);
|
await Event.Off("malwareScan.status", this);
|
||||||
await Event.Off("malwareScan.result", this);
|
await Event.Off("malwareScan.result", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PurgeSelected()
|
||||||
|
{
|
||||||
|
int users = 0;
|
||||||
|
int servers = 0;
|
||||||
|
|
||||||
|
int allServersCount = Table.FilteredItems.Count();
|
||||||
|
int position = 0;
|
||||||
|
|
||||||
|
await ToastService.CreateProcessToast("purgeProcess", "Purging");
|
||||||
|
|
||||||
|
foreach (var item in Table.FilteredItems)
|
||||||
|
{
|
||||||
|
position++;
|
||||||
|
|
||||||
|
if (item == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var server = ServerRepository.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.FirstOrDefault(x => x.Id == item.Id);
|
||||||
|
|
||||||
|
if(server == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await ToastService.UpdateProcessToast("purgeProcess", $"[{position}/{allServersCount}] {server.Name}");
|
||||||
|
|
||||||
|
ScanResults.Remove(item);
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
// Owner
|
||||||
|
|
||||||
|
server.Owner.Status = UserStatus.Banned;
|
||||||
|
UserRepository.Update(server.Owner);
|
||||||
|
users++;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SessionServerService.ReloadUserSessions(server.Owner);
|
||||||
|
}
|
||||||
|
catch (Exception) {/* Ignored */}
|
||||||
|
|
||||||
|
// Server itself
|
||||||
|
|
||||||
|
await ServerService.SetPowerState(server, PowerSignal.Kill);
|
||||||
|
await ServerService.Delete(server);
|
||||||
|
servers++;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warn($"Error purging server: {item.Uuid}");
|
||||||
|
Logger.Warn(e);
|
||||||
|
|
||||||
|
await ToastService.Error(
|
||||||
|
$"Failed to purge server '{item.Name}': {e.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.RemoveProcessToast("purgeProcess");
|
||||||
|
await ToastService.Success($"Successfully purged {servers} servers by {users} users");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Scan()
|
||||||
|
{
|
||||||
|
ScanResults.Clear();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await MalwareBackgroundScanService.Start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ServerService.Create(Model.Name, Model.Cpu, Model.Memory, Model.Disk, Model.Owner, Model.Image, Model.Node, server =>
|
var newServer = await ServerService.Create(Model.Name, Model.Cpu, Model.Memory, Model.Disk, Model.Owner, Model.Image, Model.Node, server =>
|
||||||
{
|
{
|
||||||
server.OverrideStartup = Model.OverrideStartup;
|
server.OverrideStartup = Model.OverrideStartup;
|
||||||
server.DockerImageIndex = Model.DockerImageIndex;
|
server.DockerImageIndex = Model.DockerImageIndex;
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
await ToastService.Success(SmartTranslateService.Translate("Server successfully created"));
|
await ToastService.Success(SmartTranslateService.Translate("Server successfully created"));
|
||||||
NavigationManager.NavigateTo("/admin/servers");
|
NavigationManager.NavigateTo($"/server/{newServer.Uuid}");
|
||||||
}
|
}
|
||||||
catch (DisplayException e)
|
catch (DisplayException e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,13 +6,16 @@
|
|||||||
@using ApexCharts
|
@using ApexCharts
|
||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.Shared.Components.Navigations
|
||||||
|
|
||||||
@inject StatisticsViewService StatisticsViewService
|
@inject StatisticsViewService StatisticsViewService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminStatistics))]
|
@attribute [PermissionRequired(nameof(Permissions.AdminStatistics))]
|
||||||
|
|
||||||
<div class="row mt-4 mb-2">
|
<AdminStatisticsNavigation />
|
||||||
|
|
||||||
|
<div class="row mb-2">
|
||||||
<div class="col-12 col-lg-6 col-xl">
|
<div class="col-12 col-lg-6 col-xl">
|
||||||
<div class="card card-body">
|
<div class="card card-body">
|
||||||
<select class="form-select" @bind="TimeSpanBind">
|
<select class="form-select" @bind="TimeSpanBind">
|
||||||
204
Moonlight/Shared/Views/Admin/Statistics/Live.razor
Normal file
204
Moonlight/Shared/Views/Admin/Statistics/Live.razor
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
@page "/admin/statistics/live"
|
||||||
|
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
@using Moonlight.Shared.Components.Navigations
|
||||||
|
|
||||||
|
@inject NodeService NodeService
|
||||||
|
@inject Repository<Node> NodeRepository
|
||||||
|
@inject IServiceScopeFactory ServiceScopeFactory
|
||||||
|
|
||||||
|
@attribute [PermissionRequired(nameof(Permissions.AdminStatisticsLive))]
|
||||||
|
|
||||||
|
<AdminStatisticsNavigation />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-3 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pt-5">
|
||||||
|
<div class="card-title d-flex flex-column">
|
||||||
|
<span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Math.Round(TotalCpuUsed, 2))% / 100%</span>
|
||||||
|
<span class="text-white opacity-75 pt-1 fw-semibold fs-6">
|
||||||
|
<TL>Total cpu load</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body d-flex align-items-end pt-0">
|
||||||
|
<div class="d-flex align-items-center flex-column mt-3 w-100">
|
||||||
|
@{
|
||||||
|
var cpuPercent = Math.Round(Formatter.CalculatePercentage(TotalCpuUsed, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end fw-bold fs-6 text-white opacity-75 w-100 mt-auto mb-2">
|
||||||
|
<span>@(cpuPercent)%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-8px mx-3 w-100 bg-white bg-opacity-50 rounded">
|
||||||
|
<div class="bg-@(GetStateColor(cpuPercent)) rounded h-8px" role="progressbar" style="width: @(cpuPercent)%;" aria-valuenow="@(cpuPercent)" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-3 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pt-5">
|
||||||
|
<div class="card-title d-flex flex-column">
|
||||||
|
<span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(ByteSizeValue.FromKiloBytes(TotalMemoryUsed).GigaBytes)GB / @(ByteSizeValue.FromKiloBytes(TotalMemory).GigaBytes)GB</span>
|
||||||
|
<span class="text-white opacity-75 pt-1 fw-semibold fs-6">
|
||||||
|
<TL>Total memory load</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body d-flex align-items-end pt-0">
|
||||||
|
<div class="d-flex align-items-center flex-column mt-3 w-100">
|
||||||
|
@{
|
||||||
|
var memoryPercent = Math.Round(Formatter.CalculatePercentage(TotalMemoryUsed, TotalMemory));
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end fw-bold fs-6 text-white opacity-75 w-100 mt-auto mb-2">
|
||||||
|
<span>@(memoryPercent)%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-8px mx-3 w-100 bg-white bg-opacity-50 rounded">
|
||||||
|
<div class="bg-@(GetStateColor(memoryPercent)) rounded h-8px" role="progressbar" style="width: @(memoryPercent)%;" aria-valuenow="@(memoryPercent)" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-3 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-5">
|
||||||
|
<div class="card-title d-flex flex-column">
|
||||||
|
<span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Users)</span>
|
||||||
|
<span class="text-white opacity-75 pt-1 fw-semibold fs-6">
|
||||||
|
<TL>Total user count</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-3 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-5">
|
||||||
|
<div class="card-title d-flex flex-column">
|
||||||
|
<span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Sessions)</span>
|
||||||
|
<span class="text-white opacity-75 pt-1 fw-semibold fs-6">
|
||||||
|
<TL>Total session count</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-3 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-5">
|
||||||
|
<div class="card-title d-flex flex-column">
|
||||||
|
<span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(ActiveUsers)</span>
|
||||||
|
<span class="text-white opacity-75 pt-1 fw-semibold fs-6">
|
||||||
|
<TL>Total active user count</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private long TotalMemoryUsed;
|
||||||
|
private long TotalMemory;
|
||||||
|
|
||||||
|
private double TotalCpuUsed;
|
||||||
|
|
||||||
|
private int Users;
|
||||||
|
private int ActiveUsers;
|
||||||
|
private int Sessions;
|
||||||
|
|
||||||
|
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Monitor();
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Monitor()
|
||||||
|
{
|
||||||
|
async Task Nodes()
|
||||||
|
{
|
||||||
|
TotalMemory = 0;
|
||||||
|
TotalMemoryUsed = 0;
|
||||||
|
|
||||||
|
var cpuValues = new List<double>();
|
||||||
|
|
||||||
|
foreach (var node in NodeRepository.Get().ToArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var metrics = await NodeService.GetMemoryMetrics(node);
|
||||||
|
|
||||||
|
TotalMemory += metrics.Total;
|
||||||
|
TotalMemoryUsed += metrics.Used;
|
||||||
|
|
||||||
|
var cpuMetrics = await NodeService.GetCpuMetrics(node);
|
||||||
|
cpuValues.Add(cpuMetrics.CpuUsage);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TotalCpuUsed = Formatter.CalculateAverage(cpuValues);
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UsersAndSessions()
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
var sessionService = scope.ServiceProvider.GetRequiredService<SessionServerService>();
|
||||||
|
|
||||||
|
Users = userRepo.Get().Count();
|
||||||
|
Sessions = (await sessionService.GetSessions()).Length;
|
||||||
|
ActiveUsers = userRepo
|
||||||
|
.Get()
|
||||||
|
.Count(x => x.LastVisitedAt > DateTime.UtcNow.AddDays(-1));
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Nodes();
|
||||||
|
await UsersAndSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStateColor(double percent)
|
||||||
|
{
|
||||||
|
if (percent < 60)
|
||||||
|
return "success";
|
||||||
|
else if (percent >= 60 && percent < 80)
|
||||||
|
return "warning";
|
||||||
|
else
|
||||||
|
return "danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +1,278 @@
|
|||||||
@page "/admin/support"
|
@page "/admin/support"
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Events
|
@using Moonlight.App.Models.Misc
|
||||||
@using Moonlight.App.Services.SupportChat
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using BlazorTable
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
|
||||||
@inject SupportChatServerService ServerService
|
@inject Repository<Ticket> TicketRepository
|
||||||
@inject EventSystem Event
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
||||||
|
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
<div class="row mb-5">
|
||||||
<div class="card">
|
<LazyLoader Load="LoadStatistics">
|
||||||
|
<div class="col-12 col-lg-6 col-xl">
|
||||||
|
<div class="mt-4 card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-column flex-xl-row p-5 pb-0">
|
<div class="row align-items-center gx-0">
|
||||||
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
<div class="col">
|
||||||
<div class="mb-0">
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
<h1 class="text-dark mb-6">
|
<TL>Total Tickets</TL>
|
||||||
<TL>Open chats</TL>
|
</h6>
|
||||||
</h1>
|
<span class="h2 mb-0">
|
||||||
<div class="separator"></div>
|
@(TotalTicketCount)
|
||||||
<div class="mb-5">
|
|
||||||
@if (OpenChats.Any())
|
|
||||||
{
|
|
||||||
foreach (var chat in OpenChats)
|
|
||||||
{
|
|
||||||
<div class="d-flex mt-3 mb-3 ms-2 me-2">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td rowspan="2">
|
|
||||||
<span class="svg-icon svg-icon-2x me-5 ms-n1 svg-icon-success">
|
|
||||||
<i class="text-primary bx bx-md bx-message-dots"></i>
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="col-auto">
|
||||||
<a href="/admin/support/view/@(chat.Key.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
<span class="h2 text-muted mb-0">
|
||||||
@(chat.Key.FirstName) @(chat.Key.LastName)
|
<i class="text-primary bx bx-purchase-tag bx-lg"></i>
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<span class="text-muted fw-semibold fs-6">
|
|
||||||
@if (chat.Value == null)
|
|
||||||
{
|
|
||||||
<TL>No message sent yet</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@(chat.Value.Content)
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="separator"></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<TL>No support chat is currently open</TL>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 col-xl">
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Unassigned tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(UnAssignedTicketCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-0">
|
||||||
|
<i class="text-primary bx bxs-bell-ring bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 col-xl">
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Pending tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(PendingTicketCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-hourglass bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 col-xl">
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Closed tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(ClosedTicketCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-lock bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<TL>Ticket overview</TL>
|
||||||
|
</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Overview"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(0)" />
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Unassigned tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(1)" />
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("My tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(2)" />
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("All tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(3)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader @ref="TicketLazyLoader" Load="LoadTickets">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<Table TableItem="Ticket" Items="AllTickets" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Assigned to"))" Field="@(x => x.AssignedTo)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.AssignedTo == null ? "None" : context.AssignedTo.FirstName + " " + context.AssignedTo.LastName)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Ticket title"))" Field="@(x => x.IssueTopic)" Filterable="true" Sortable="false"/>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("User"))" Field="@(x => x.CreatedBy)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.CreatedBy.FirstName) @(context.CreatedBy.LastName)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Created at"))" Field="@(x => x.CreatedAt)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>@(Formatter.FormatDate(context.CreatedAt))</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Priority"))" Field="@(x => x.Priority)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@switch (context.Priority)
|
||||||
|
{
|
||||||
|
case TicketPriority.Low:
|
||||||
|
<span class="badge bg-success">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Medium:
|
||||||
|
<span class="badge bg-primary">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.High:
|
||||||
|
<span class="badge bg-warning">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Critical:
|
||||||
|
<span class="badge bg-danger">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Status)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@switch (context.Status)
|
||||||
|
{
|
||||||
|
case TicketStatus.Closed:
|
||||||
|
<span class="badge bg-danger">@(context.Status)</span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.Open:
|
||||||
|
<span class="badge bg-success">@(context.Status)</span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.Pending:
|
||||||
|
<span class="badge bg-warning">@(context.Status)</span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.WaitingForUser:
|
||||||
|
<span class="badge bg-primary">@(context.Status)</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a class="btn btn-sm btn-primary" href="/admin/support/view/@(context.Id)">
|
||||||
|
<TL>Open</TL>
|
||||||
|
</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private LazyLoader? LazyLoader;
|
private int TotalTicketCount;
|
||||||
private Dictionary<User, SupportChatMessage?> OpenChats = new();
|
private int UnAssignedTicketCount;
|
||||||
|
private int PendingTicketCount;
|
||||||
|
private int ClosedTicketCount;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
private Ticket[] AllTickets;
|
||||||
|
private int Filter = 0;
|
||||||
|
|
||||||
|
private LazyLoader TicketLazyLoader;
|
||||||
|
|
||||||
|
private Task LoadStatistics(LazyLoader _)
|
||||||
{
|
{
|
||||||
await Event.On<User>("supportChat.new", this, async user =>
|
TotalTicketCount = TicketRepository
|
||||||
{
|
.Get()
|
||||||
//TODO: Play sound or smth. Add a config option
|
.Count();
|
||||||
|
|
||||||
OpenChats = await ServerService.GetOpenChats();
|
UnAssignedTicketCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.Count(x => x.AssignedTo == null);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
PendingTicketCount = TicketRepository
|
||||||
});
|
.Get()
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.AssignedTo != null)
|
||||||
|
.Count(x => x.Status != TicketStatus.Closed);
|
||||||
|
|
||||||
|
ClosedTicketCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Count(x => x.Status == TicketStatus.Closed);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg) // Only for initial load
|
private Task LoadTickets(LazyLoader _)
|
||||||
{
|
{
|
||||||
OpenChats = await ServerService.GetOpenChats();
|
switch (Filter)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.ToArray();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.AssignedTo == null)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.ToArray();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.AssignedTo != null)
|
||||||
|
.Where(x => x.AssignedTo!.Id == IdentityService.User.Id)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.ToArray();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.ToArray();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Dispose()
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateFilter(int filterId)
|
||||||
{
|
{
|
||||||
await Event.Off("supportChat.new", this);
|
Filter = filterId;
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
379
Moonlight/Shared/Views/Admin/Support/Old_Index.razor
Normal file
379
Moonlight/Shared/Views/Admin/Support/Old_Index.razor
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
@page "/old_admin/support"
|
||||||
|
@page "/old_admin/support/{Id:int}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Services.Tickets
|
||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Events
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Models.Misc
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
@using Moonlight.Shared.Components.Tickets
|
||||||
|
|
||||||
|
@inject TicketAdminService AdminService
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject EventSystem EventSystem
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
|
||||||
|
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<div class="d-flex flex-column flex-lg-row">
|
||||||
|
<div class="flex-column flex-lg-row-auto w-100 w-lg-300px w-xl-400px mb-10 mb-lg-0">
|
||||||
|
<div class="card card-flush">
|
||||||
|
<div class="card-body pt-5">
|
||||||
|
<div class="scroll-y me-n5 pe-5 h-200px h-lg-auto" data-kt-scroll="true" data-kt-scroll-activate="{default: false, lg: true}" data-kt-scroll-max-height="auto" data-kt-scroll-dependencies="#kt_header, #kt_app_header, #kt_toolbar, #kt_app_toolbar, #kt_footer, #kt_app_footer, #kt_chat_contacts_header" data-kt-scroll-wrappers="#kt_content, #kt_app_content, #kt_chat_contacts_body" data-kt-scroll-offset="5px" style="max-height: 601px;">
|
||||||
|
<div class="separator separator-content border-primary mb-10 mt-5">
|
||||||
|
<span class="w-250px fw-bold fs-5">
|
||||||
|
<TL>Unassigned tickets</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var ticket in UnAssignedTickets)
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-stack py-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="ms-5">
|
||||||
|
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
|
||||||
|
@if (ticket.Value != null)
|
||||||
|
{
|
||||||
|
<div class="fw-semibold text-muted">
|
||||||
|
@(ticket.Value.Content)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column align-items-end ms-2">
|
||||||
|
@if (ticket.Value != null)
|
||||||
|
{
|
||||||
|
<span class="text-muted fs-7 mb-1">
|
||||||
|
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if (ticket.Key != UnAssignedTickets.Last().Key)
|
||||||
|
{
|
||||||
|
<div class="separator"></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (AssignedTickets.Any())
|
||||||
|
{
|
||||||
|
<div class="separator separator-content border-primary mb-5 mt-8">
|
||||||
|
<span class="w-250px fw-bold fs-5">
|
||||||
|
<TL>Assigned tickets</TL>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var ticket in AssignedTickets)
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-stack py-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="ms-5">
|
||||||
|
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
|
||||||
|
@if (ticket.Value != null)
|
||||||
|
{
|
||||||
|
<div class="fw-semibold text-muted">
|
||||||
|
@(ticket.Value.Content)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column align-items-end ms-2">
|
||||||
|
@if (ticket.Value != null)
|
||||||
|
{
|
||||||
|
<span class="text-muted fs-7 mb-1">
|
||||||
|
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if (ticket.Key != AssignedTickets.Last().Key)
|
||||||
|
{
|
||||||
|
<div class="separator"></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-lg-row-fluid ms-lg-7 ms-xl-10">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
@if (AdminService.Ticket != null)
|
||||||
|
{
|
||||||
|
<div class="card-title">
|
||||||
|
<div class="d-flex justify-content-center flex-column me-3">
|
||||||
|
<span class="fs-3 fw-bold text-gray-900 me-1 mb-2 lh-1">@(AdminService.Ticket.IssueTopic)</span>
|
||||||
|
<div class="mb-0 lh-1">
|
||||||
|
<span class="fs-6 fw-bold text-muted me-2">
|
||||||
|
<TL>Status</TL>
|
||||||
|
</span>
|
||||||
|
@switch (AdminService.Ticket.Status)
|
||||||
|
{
|
||||||
|
case TicketStatus.Closed:
|
||||||
|
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.Open:
|
||||||
|
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.Pending:
|
||||||
|
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketStatus.WaitingForUser:
|
||||||
|
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
<span class="fs-6 fw-semibold text-muted me-5">@(AdminService.Ticket.Status)</span>
|
||||||
|
|
||||||
|
<span class="fs-6 fw-bold text-muted me-2">
|
||||||
|
<TL>Priority</TL>
|
||||||
|
</span>
|
||||||
|
@switch (AdminService.Ticket.Priority)
|
||||||
|
{
|
||||||
|
case TicketPriority.Low:
|
||||||
|
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Medium:
|
||||||
|
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.High:
|
||||||
|
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Critical:
|
||||||
|
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
<span class="fs-6 fw-semibold text-muted">@(AdminService.Ticket.Priority)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="me-3">
|
||||||
|
@if (AdminService.Ticket!.AssignedTo == null)
|
||||||
|
{
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Claim"))"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Unclaim"))"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<select @bind="Priority" class="form-select rounded-start">
|
||||||
|
@foreach (var priority in (TicketPriority[])Enum.GetValues(typeof(TicketPriority)))
|
||||||
|
{
|
||||||
|
if (Priority == priority)
|
||||||
|
{
|
||||||
|
<option value="@(priority)" selected="">@(priority)</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<option value="@(priority)">@(priority)</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Update priority"))"
|
||||||
|
CssClasses="btn-primary"
|
||||||
|
OnClick="UpdatePriority">
|
||||||
|
</WButton>
|
||||||
|
<select @bind="Status" class="form-select">
|
||||||
|
@foreach (var status in (TicketStatus[])Enum.GetValues(typeof(TicketStatus)))
|
||||||
|
{
|
||||||
|
if (Status == status)
|
||||||
|
{
|
||||||
|
<option value="@(status)" selected="">@(status)</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<option value="@(status)">@(status)</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Update status"))"
|
||||||
|
CssClasses="btn-primary"
|
||||||
|
OnClick="UpdateStatus">
|
||||||
|
</WButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="card-title">
|
||||||
|
<div class="d-flex justify-content-center flex-column me-3">
|
||||||
|
<span class="fs-4 fw-bold text-gray-900 me-1 mb-2 lh-1">
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||||
|
@if (AdminService.Ticket == null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<TicketMessageView Messages="Messages" ViewAsSupport="true"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (AdminService.Ticket != null)
|
||||||
|
{
|
||||||
|
<div class="card-footer pt-4" id="kt_chat_messenger_footer">
|
||||||
|
<div class="d-flex flex-stack">
|
||||||
|
<table class="w-100">
|
||||||
|
<tr>
|
||||||
|
<td class="align-top">
|
||||||
|
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
|
||||||
|
</td>
|
||||||
|
<td class="w-100">
|
||||||
|
<textarea @bind="MessageText" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
|
||||||
|
</td>
|
||||||
|
<td class="align-top">
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Send"))"
|
||||||
|
WorkingText="@(SmartTranslateService.Translate("Sending"))"
|
||||||
|
CssClasses="btn-primary ms-2"
|
||||||
|
OnClick="SendMessage">
|
||||||
|
</WButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
private Dictionary<Ticket, TicketMessage?> AssignedTickets;
|
||||||
|
private Dictionary<Ticket, TicketMessage?> UnAssignedTickets;
|
||||||
|
private List<TicketMessage> Messages = new();
|
||||||
|
private string MessageText;
|
||||||
|
private SmartFileSelect FileSelect;
|
||||||
|
|
||||||
|
private TicketPriority Priority;
|
||||||
|
private TicketStatus Status;
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
await Unsubscribe();
|
||||||
|
await ReloadTickets();
|
||||||
|
await Subscribe();
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdatePriority()
|
||||||
|
{
|
||||||
|
await AdminService.UpdatePriority(Priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateStatus()
|
||||||
|
{
|
||||||
|
await AdminService.UpdateStatus(Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null)
|
||||||
|
MessageText = "File upload";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(MessageText))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = await AdminService.Send(MessageText, FileSelect.SelectedFile);
|
||||||
|
Messages.Add(msg);
|
||||||
|
MessageText = "";
|
||||||
|
FileSelect.SelectedFile = null;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Subscribe()
|
||||||
|
{
|
||||||
|
await EventSystem.On<Ticket>("tickets.new", this, async _ =>
|
||||||
|
{
|
||||||
|
await ReloadTickets(false);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (AdminService.Ticket != null)
|
||||||
|
{
|
||||||
|
await EventSystem.On<TicketMessage>($"tickets.{AdminService.Ticket.Id}.message", this, async message =>
|
||||||
|
{
|
||||||
|
if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && message.IsSupportMessage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Messages.Add(message);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventSystem.On<Ticket>($"tickets.{AdminService.Ticket.Id}.status", this, async _ =>
|
||||||
|
{
|
||||||
|
await ReloadTickets(false);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Unsubscribe()
|
||||||
|
{
|
||||||
|
await EventSystem.Off("tickets.new", this);
|
||||||
|
|
||||||
|
if (AdminService.Ticket != null)
|
||||||
|
{
|
||||||
|
await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.message", this);
|
||||||
|
await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.status", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadTickets(bool reloadMessages = true)
|
||||||
|
{
|
||||||
|
AdminService.Ticket = null;
|
||||||
|
//AssignedTickets = await AdminService.GetAssigned();
|
||||||
|
//UnAssignedTickets = await AdminService.GetUnAssigned();
|
||||||
|
|
||||||
|
if (Id != 0)
|
||||||
|
{
|
||||||
|
AdminService.Ticket = AssignedTickets
|
||||||
|
.FirstOrDefault(x => x.Key.Id == Id)
|
||||||
|
.Key ?? null;
|
||||||
|
|
||||||
|
if (AdminService.Ticket == null)
|
||||||
|
{
|
||||||
|
AdminService.Ticket = UnAssignedTickets
|
||||||
|
.FirstOrDefault(x => x.Key.Id == Id)
|
||||||
|
.Key ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdminService.Ticket == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Status = AdminService.Ticket.Status;
|
||||||
|
Priority = AdminService.Ticket.Priority;
|
||||||
|
|
||||||
|
if (reloadMessages)
|
||||||
|
{
|
||||||
|
var msgs = await AdminService.GetMessages();
|
||||||
|
Messages = msgs.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
await Unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user