Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
388deacf60 | ||
|
|
aa547038de | ||
|
|
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")]
|
||||
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; } = 500;
|
||||
|
||||
[JsonProperty("Auth")] public AuthData Auth { 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("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
|
||||
@@ -276,6 +294,10 @@ public class ConfigV1
|
||||
[Blur]
|
||||
public string Token { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[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();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ public class DataContext : DbContext
|
||||
public DbSet<IpBan> IpBans { get; set; }
|
||||
public DbSet<PermissionGroup> PermissionGroups { 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)
|
||||
{
|
||||
|
||||
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("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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -690,6 +714,97 @@ namespace Moonlight.App.Database.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -842,6 +957,21 @@ namespace Moonlight.App.Database.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
||||
@@ -1000,6 +1130,36 @@ namespace Moonlight.App.Database.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
|
||||
@@ -1055,6 +1215,11 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||
{
|
||||
b.Navigation("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
||||
{
|
||||
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,9 @@ public class DatabaseCheckupService
|
||||
{
|
||||
Logger.Info($"{migrations.Length} migrations pending. Updating now");
|
||||
|
||||
await BackupDatabase();
|
||||
var backupHelper = new BackupHelper();
|
||||
await backupHelper.CreateBackup(
|
||||
PathBuilder.File("storage", "backups", $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip"));
|
||||
|
||||
Logger.Info("Applying migrations");
|
||||
|
||||
@@ -58,53 +60,4 @@ public class DatabaseCheckupService
|
||||
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}"
|
||||
);
|
||||
|
||||
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,
|
||||
Size = response.File ? response.Size : 0,
|
||||
IsFile = response.File,
|
||||
result.Add(new()
|
||||
{
|
||||
Name = resItem.Name,
|
||||
Size = resItem.File ? resItem.Size : 0,
|
||||
IsFile = resItem.File,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return x.ToArray();
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public override Task Cd(string dir)
|
||||
|
||||
@@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Http.Requests.Daemon;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Background;
|
||||
|
||||
namespace Moonlight.App.Http.Controllers.Api.Remote;
|
||||
|
||||
@@ -10,19 +11,17 @@ namespace Moonlight.App.Http.Controllers.Api.Remote;
|
||||
[Route("api/remote/ddos")]
|
||||
public class DdosController : Controller
|
||||
{
|
||||
private readonly NodeRepository NodeRepository;
|
||||
private readonly EventSystem Event;
|
||||
private readonly DdosAttackRepository DdosAttackRepository;
|
||||
private readonly Repository<Node> NodeRepository;
|
||||
private readonly DdosProtectionService DdosProtectionService;
|
||||
|
||||
public DdosController(NodeRepository nodeRepository, EventSystem eventSystem, DdosAttackRepository ddosAttackRepository)
|
||||
public DdosController(Repository<Node> nodeRepository, DdosProtectionService ddosProtectionService)
|
||||
{
|
||||
NodeRepository = nodeRepository;
|
||||
Event = eventSystem;
|
||||
DdosAttackRepository = ddosAttackRepository;
|
||||
DdosProtectionService = ddosProtectionService;
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> Update([FromBody] DdosStatus ddosStatus)
|
||||
[HttpPost("start")]
|
||||
public async Task<ActionResult> Start([FromBody] DdosStart ddosStart)
|
||||
{
|
||||
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||
var id = tokenData.Split(".")[0];
|
||||
@@ -36,17 +35,25 @@ public class DdosController : Controller
|
||||
if (token != node.Token)
|
||||
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,
|
||||
Data = ddosStatus.Data,
|
||||
Ip = ddosStatus.Ip,
|
||||
Node = node
|
||||
};
|
||||
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||
var id = tokenData.Split(".")[0];
|
||||
var token = tokenData.Split(".")[1];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
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; }
|
||||
}
|
||||
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"
|
||||
};
|
||||
|
||||
public static Permission AdminSysPlugins = new()
|
||||
{
|
||||
Index = 2,
|
||||
Name = "Admin system plugins",
|
||||
Description = "View and install plugins"
|
||||
};
|
||||
|
||||
public static Permission AdminDomains = new()
|
||||
{
|
||||
Index = 4,
|
||||
@@ -44,13 +51,6 @@ public static class Permissions
|
||||
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()
|
||||
{
|
||||
Index = 9,
|
||||
@@ -401,6 +401,13 @@ public static class Permissions
|
||||
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? FromString(string name)
|
||||
{
|
||||
var type = typeof(Permissions);
|
||||
|
||||
15
Moonlight/App/Plugin/MoonlightPlugin.cs
Normal file
15
Moonlight/App/Plugin/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
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; }
|
||||
}
|
||||
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; }
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class DiscordNotificationService
|
||||
Event.On<User>("supportChat.close", this, OnSupportChatClose);
|
||||
Event.On<User>("user.rating", this, OnUserRated);
|
||||
Event.On<User>("billing.completed", this, OnBillingCompleted);
|
||||
Event.On<BlocklistIp>("ddos.add", this, OnIpBlockListed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -43,6 +44,18 @@ public class DiscordNotificationService
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
await SendNotification("", builder =>
|
||||
|
||||
@@ -19,6 +19,7 @@ public class MalwareScanService
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool ScanAllServers { get; set; }
|
||||
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
|
||||
public string Status { get; private set; } = "N/A";
|
||||
|
||||
@@ -26,7 +27,6 @@ public class MalwareScanService
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
Event = eventSystem;
|
||||
|
||||
ScanResults = new();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ public class MalwareScanService
|
||||
|
||||
private async Task Run()
|
||||
{
|
||||
// Clean results
|
||||
IsRunning = true;
|
||||
Status = "Clearing last results";
|
||||
await Event.Emit("malwareScan.status", IsRunning);
|
||||
@@ -53,6 +54,55 @@ public class MalwareScanService
|
||||
|
||||
await Event.Emit("malwareScan.result");
|
||||
|
||||
// Load servers to scan
|
||||
|
||||
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>();
|
||||
|
||||
Status = "Fetching servers to scan";
|
||||
await Event.Emit("malwareScan.status", IsRunning);
|
||||
|
||||
Server[] servers;
|
||||
|
||||
if (ScanAllServers)
|
||||
servers = ServerRepository.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 results = await PerformScanOnServer(server);
|
||||
|
||||
if (results.Any())
|
||||
{
|
||||
lock (ScanResults)
|
||||
{
|
||||
ScanResults.Add(server, results);
|
||||
}
|
||||
|
||||
await Event.Emit("malwareScan.result");
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
IsRunning = false;
|
||||
await Event.Emit("malwareScan.status", IsRunning);
|
||||
}
|
||||
|
||||
private async Task<Server[]> GetOnlineServers()
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
// Load services from di scope
|
||||
@@ -103,42 +153,10 @@ public class MalwareScanService
|
||||
}
|
||||
}
|
||||
|
||||
// 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}");
|
||||
}
|
||||
return containerServerMapped.Keys.ToArray();
|
||||
}
|
||||
|
||||
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)
|
||||
private async Task<MalwareScanResult[]> PerformScanOnServer(Server server)
|
||||
{
|
||||
var results = new List<MalwareScanResult>();
|
||||
|
||||
@@ -162,6 +180,29 @@ public class MalwareScanService
|
||||
}
|
||||
}
|
||||
|
||||
async Task ScanMinerJar()
|
||||
{
|
||||
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))
|
||||
{
|
||||
results.Add(new ()
|
||||
{
|
||||
Title = "Found Miner",
|
||||
Description = "Detected suspicious library directory which may contain a script for miners",
|
||||
Author = "Marcel Baumgartner"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task ScanFakePlayerPlugins()
|
||||
{
|
||||
var access = await ServerService.CreateFileAccess(server, null!);
|
||||
@@ -190,6 +231,7 @@ public class MalwareScanService
|
||||
// Execute scans
|
||||
await ScanSelfBot();
|
||||
await ScanFakePlayerPlugins();
|
||||
await ScanMinerJar();
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public class StorageService
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
||||
|
||||
if(IsEmpty(PathBuilder.Dir("storage", "resources")))
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ public class MoonlightService
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -50,6 +50,11 @@ public class NodeService
|
||||
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)
|
||||
{
|
||||
await DaemonApiHelper.Post(node, "mount", new Mount()
|
||||
|
||||
100
Moonlight/App/Services/Plugins/PluginService.cs
Normal file
100
Moonlight/App/Services/Plugins/PluginService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Moonlight.App.Helpers;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public class IdentityService
|
||||
if (user == null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
95
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
95
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
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<Dictionary<Ticket, TicketMessage?>> GetAssigned()
|
||||
{
|
||||
return await TicketServerService.GetUserAssignedTickets(IdentityService.User);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Ticket, TicketMessage?>> GetUnAssigned()
|
||||
{
|
||||
return await TicketServerService.GetUnAssignedTickets();
|
||||
}
|
||||
|
||||
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,
|
||||
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 Claim()
|
||||
{
|
||||
await TicketServerService.Claim(Ticket!, IdentityService.User);
|
||||
}
|
||||
|
||||
public async Task UnClaim()
|
||||
{
|
||||
await TicketServerService.Claim(Ticket!);
|
||||
}
|
||||
}
|
||||
68
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
68
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
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<Dictionary<Ticket, TicketMessage?>> Get()
|
||||
{
|
||||
return await TicketServerService.GetUserTickets(IdentityService.User);
|
||||
}
|
||||
|
||||
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!);
|
||||
}
|
||||
}
|
||||
249
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
249
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
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);
|
||||
//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<Dictionary<Ticket, TicketMessage?>> GetUserTickets(User u)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||
|
||||
var tickets = ticketRepo
|
||||
.Get()
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.Messages)
|
||||
.Where(x => x.CreatedBy.Id == u.Id)
|
||||
.Where(x => x.Status != TicketStatus.Closed)
|
||||
.ToArray();
|
||||
|
||||
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
var message = ticket.Messages
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
result.Add(ticket, message);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
public Task<Dictionary<Ticket, TicketMessage?>> GetUserAssignedTickets(User u)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||
|
||||
var tickets = ticketRepo
|
||||
.Get()
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.Messages)
|
||||
.Where(x => x.Status != TicketStatus.Closed)
|
||||
.Where(x => x.AssignedTo.Id == u.Id)
|
||||
.ToArray();
|
||||
|
||||
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
var message = ticket.Messages
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
result.Add(ticket, message);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
public Task<Dictionary<Ticket, TicketMessage?>> GetUnAssignedTickets()
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||
|
||||
var tickets = ticketRepo
|
||||
.Get()
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.Messages)
|
||||
.Include(x => x.AssignedTo)
|
||||
.Where(x => x.AssignedTo == null)
|
||||
.Where(x => x.Status != TicketStatus.Closed)
|
||||
.ToArray();
|
||||
|
||||
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
var message = ticket.Messages
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
result.Add(ticket, message);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
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)
|
||||
.First(x => x.Id == ticket.Id);
|
||||
|
||||
return Task.FromResult(tickets.Messages.ToArray());
|
||||
}
|
||||
|
||||
public async Task Claim(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);
|
||||
}
|
||||
}
|
||||
@@ -67,16 +67,14 @@ public class UserService
|
||||
throw new DisplayException("The email is already in use");
|
||||
}
|
||||
|
||||
//TODO: Validation
|
||||
|
||||
// Add user
|
||||
var user = UserRepository.Add(new()
|
||||
{
|
||||
Address = "",
|
||||
Admin = false,
|
||||
Admin = !UserRepository.Get().Any(),
|
||||
City = "",
|
||||
Country = "",
|
||||
Email = email,
|
||||
Email = email.ToLower(),
|
||||
Password = BCrypt.Net.BCrypt.HashPassword(password),
|
||||
FirstName = firstname,
|
||||
LastName = lastname,
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="MineStat" Version="3.1.1" />
|
||||
<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="Otp.NET" Version="1.3.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
|
||||
<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.Serilog" Version="3.33.1" />
|
||||
<PackageReference Include="Serilog" Version="3.0.0" />
|
||||
@@ -90,6 +90,7 @@
|
||||
<Folder Include="App\ApiClients\CloudPanel\Resources\" />
|
||||
<Folder Include="App\Http\Middleware" />
|
||||
<Folder Include="storage\backups\" />
|
||||
<Folder Include="storage\plugins\" />
|
||||
<Folder Include="storage\resources\public\background\" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -111,12 +112,4 @@
|
||||
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerResetSetting.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="storage\configs\config.json.bak">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -25,9 +25,11 @@ using Moonlight.App.Services.Interop;
|
||||
using Moonlight.App.Services.Mail;
|
||||
using Moonlight.App.Services.Minecraft;
|
||||
using Moonlight.App.Services.Notifications;
|
||||
using Moonlight.App.Services.Plugins;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
using Moonlight.App.Services.Statistics;
|
||||
using Moonlight.App.Services.SupportChat;
|
||||
using Moonlight.App.Services.Tickets;
|
||||
using Sentry;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
@@ -110,6 +112,9 @@ namespace Moonlight
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var pluginService = new PluginService();
|
||||
await pluginService.BuildServices(builder.Services);
|
||||
|
||||
// Switch to logging.net injection
|
||||
// TODO: Enable in production
|
||||
builder.Logging.ClearProviders();
|
||||
@@ -208,6 +213,10 @@ namespace Moonlight
|
||||
builder.Services.AddScoped<PopupService>();
|
||||
builder.Services.AddScoped<SubscriptionService>();
|
||||
builder.Services.AddScoped<BillingService>();
|
||||
builder.Services.AddSingleton<PluginStoreService>();
|
||||
builder.Services.AddSingleton<TicketServerService>();
|
||||
builder.Services.AddScoped<TicketClientService>();
|
||||
builder.Services.AddScoped<TicketAdminService>();
|
||||
|
||||
builder.Services.AddScoped<SessionClientService>();
|
||||
builder.Services.AddSingleton<SessionServerService>();
|
||||
@@ -239,6 +248,8 @@ namespace Moonlight
|
||||
builder.Services.AddSingleton<MalwareScanService>();
|
||||
builder.Services.AddSingleton<TelemetryService>();
|
||||
builder.Services.AddSingleton<TempMailService>();
|
||||
builder.Services.AddSingleton<DdosProtectionService>();
|
||||
builder.Services.AddSingleton(pluginService);
|
||||
|
||||
// Other
|
||||
builder.Services.AddSingleton<MoonlightService>();
|
||||
@@ -289,7 +300,7 @@ namespace Moonlight
|
||||
_ = app.Services.GetRequiredService<MalwareScanService>();
|
||||
_ = app.Services.GetRequiredService<TelemetryService>();
|
||||
_ = app.Services.GetRequiredService<TempMailService>();
|
||||
|
||||
_ = app.Services.GetRequiredService<DdosProtectionService>();
|
||||
_ = app.Services.GetRequiredService<MoonlightService>();
|
||||
|
||||
// Discord bot service
|
||||
|
||||
@@ -35,6 +35,18 @@
|
||||
},
|
||||
"applicationUrl": "http://moonlight.testy: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.testy:5118;https://localhost:7118;http://localhost:5118",
|
||||
"dotnetRunMessages": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,13 @@
|
||||
@implements IDisposable
|
||||
|
||||
<div class="card bg-black rounded">
|
||||
@if (ShowHeader)
|
||||
{
|
||||
<div class="card-header">
|
||||
<span class="card-title">@(Header)</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-body">
|
||||
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
|
||||
</div>
|
||||
@@ -44,6 +51,12 @@
|
||||
[Parameter]
|
||||
public bool HideControls { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowHeader { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public string Header { get; set; } = "Header.changeme.txt";
|
||||
|
||||
// Events
|
||||
[Parameter]
|
||||
public Action<string> OnSubmit { get; set; }
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
Language="@EditorLanguage"
|
||||
OnCancel="() => Cancel()"
|
||||
OnSubmit="(_) => Save()"
|
||||
HideControls="false">
|
||||
HideControls="false"
|
||||
ShowHeader="true"
|
||||
Header="@(EditingFile.Name)">
|
||||
</FileEditor>
|
||||
}
|
||||
else
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<tbody class="fw-semibold text-gray-600">
|
||||
<LazyLoader Load="Load">
|
||||
<ContentBlock @ref="ContentBlock" AllowContentOverride="true">
|
||||
@if (Access.CurrentPath != "/")
|
||||
{
|
||||
<tr class="even">
|
||||
<td class="w-10px">
|
||||
</td>
|
||||
@@ -57,6 +59,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var file in Data)
|
||||
{
|
||||
<tr class="even">
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
<TL>Logs</TL>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 5 ? "active" : "")" href="/admin/security/ddos">
|
||||
<TL>Ddos protection</TL>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
<TL>Mail</TL>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
219
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
219
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
@@ -0,0 +1,219 @@
|
||||
@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
|
||||
|
||||
@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>
|
||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
|
||||
</div>
|
||||
<div class="symbol symbol-35px symbol-circle ">
|
||||
<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 ">
|
||||
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
|
||||
<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 ">
|
||||
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
|
||||
<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>
|
||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
|
||||
</div>
|
||||
<div class="symbol symbol-35px symbol-circle ">
|
||||
<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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.Plugin.UI.Webspaces
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Interop
|
||||
|
||||
@@ -34,26 +35,14 @@
|
||||
<div class="card mb-xl-10 mb-5">
|
||||
<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">
|
||||
@foreach (var tab in Context.Tabs)
|
||||
{
|
||||
<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)">
|
||||
<TL>Dashboard</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 class="nav-link text-active-primary ms-0 me-10 py-5 @(Route == tab.Route ? "active" : "")" href="/webspace/@(WebSpace.Id + tab.Route)">
|
||||
<TL>@(tab.Name)</TL>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,11 +50,14 @@
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public int Index { get; set; }
|
||||
public string Route { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public WebSpace WebSpace { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public WebspacePageContext Context { get; set; }
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
if (await AlertService.ConfirmMath())
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@inject IpBanService IpBanService
|
||||
@inject DynamicBackgroundService DynamicBackgroundService
|
||||
@inject KeyListenerService KeyListenerService
|
||||
@inject ConfigService ConfigService
|
||||
|
||||
@{
|
||||
var uri = new Uri(NavigationManager.Uri);
|
||||
@@ -245,6 +246,13 @@
|
||||
RunDelayedMenu(1);
|
||||
RunDelayedMenu(3);
|
||||
RunDelayedMenu(5);
|
||||
|
||||
if (ConfigService.Get().Moonlight.EnableLatencyCheck)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.loading.checkConnection",
|
||||
ConfigService.Get().Moonlight.AppUrl,
|
||||
ConfigService.Get().Moonlight.LatencyCheckThreshold);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
<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>
|
||||
|
||||
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
|
||||
.Get()
|
||||
.ToArray()
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareScanService.ScanAllServers">
|
||||
<label class="form-check-label" for="scanAllServers">
|
||||
<TL>Scan all servers</TL>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WButton Text="@(SmartTranslateService.Translate("Start scan"))"
|
||||
CssClasses="btn-success"
|
||||
OnClick="MalwareScanService.Start">
|
||||
|
||||
@@ -1,101 +1,379 @@
|
||||
@page "/admin/support"
|
||||
@page "/admin/support/{Id:int}"
|
||||
|
||||
@using Moonlight.App.Services.Tickets
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Events
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Sessions
|
||||
@using Moonlight.Shared.Components.Tickets
|
||||
|
||||
@inject SupportChatServerService ServerService
|
||||
@inject EventSystem Event
|
||||
|
||||
@implements IDisposable
|
||||
@inject TicketAdminService AdminService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject EventSystem EventSystem
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-xl-row p-5 pb-0">
|
||||
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
||||
<div class="mb-0">
|
||||
<h1 class="text-dark mb-6">
|
||||
<TL>Open chats</TL>
|
||||
</h1>
|
||||
<div class="separator"></div>
|
||||
<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>
|
||||
@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>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/support/view/@(chat.Key.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
||||
@(chat.Key.FirstName) @(chat.Key.LastName)
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-muted fw-semibold fs-6">
|
||||
@if (chat.Value == null)
|
||||
</div>
|
||||
|
||||
@foreach (var ticket in UnAssignedTickets)
|
||||
{
|
||||
<TL>No message sent yet</TL>
|
||||
<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"))" OnClick="AdminService.Claim"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(chat.Value.Content)
|
||||
<WButton Text="@(SmartTranslateService.Translate("Unclaim"))" OnClick="AdminService.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 class="separator"></div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<TL>No support chat is currently open</TL>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
private LazyLoader? LazyLoader;
|
||||
private Dictionary<User, SupportChatMessage?> OpenChats = new();
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
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 Event.On<User>("supportChat.new", this, async user =>
|
||||
await Unsubscribe();
|
||||
await ReloadTickets();
|
||||
await Subscribe();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task UpdatePriority()
|
||||
{
|
||||
//TODO: Play sound or smth. Add a config option
|
||||
await AdminService.UpdatePriority(Priority);
|
||||
}
|
||||
|
||||
OpenChats = await ServerService.GetOpenChats();
|
||||
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 Load(LazyLoader arg) // Only for initial load
|
||||
private async Task Unsubscribe()
|
||||
{
|
||||
OpenChats = await ServerService.GetOpenChats();
|
||||
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 Event.Off("supportChat.new", this);
|
||||
await Unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
@page "/admin/support/view/{Id:int}"
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
@using System.Text.RegularExpressions
|
||||
@using Moonlight.App.Services.Files
|
||||
|
||||
@inject SupportChatAdminService AdminService
|
||||
@inject UserRepository UserRepository
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject ResourceService ResourceService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
@attribute [PermissionRequired(nameof(Permissions.AdminSupportView))]
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (User == null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<TL>User not found</TL>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="d-flex flex-column flex-xl-row p-7">
|
||||
<div class="flex-lg-row-fluid me-6 mb-20 mb-xl-0">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<LazyLoader Load="LoadMessages">
|
||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||
{
|
||||
<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>
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
||||
@if (message.Sender != null)
|
||||
{
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
<TL>System</TL>
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
<div class="symbol symbol-35px symbol-circle ">
|
||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||
@if (message.Sender == null)
|
||||
{
|
||||
<TL>@(message.Content)</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var line in message.Content.Split("\n"))
|
||||
{
|
||||
@(line)<br/>
|
||||
}
|
||||
|
||||
if (message.Attachment != "")
|
||||
{
|
||||
<div class="mt-3">
|
||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||
{
|
||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||
</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 ">
|
||||
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
</a>
|
||||
<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 (message.Attachment != "")
|
||||
{
|
||||
<div class="mt-3">
|
||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||
{
|
||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@if (Typing.Any())
|
||||
{
|
||||
<span class="mb-5 fs-5 d-flex flex-row">
|
||||
<div class="wave me-1">
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px"></div>
|
||||
</div>
|
||||
@if (Typing.Length > 1)
|
||||
{
|
||||
<span>
|
||||
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@(Typing.First()) <TL>is typing</TL>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<table class="w-100">
|
||||
<tr>
|
||||
<td class="align-top">
|
||||
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
||||
</td>
|
||||
<td class="w-100">
|
||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
||||
</textarea>
|
||||
</td>
|
||||
<td class="align-top">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Send"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Sending"))"
|
||||
CssClasses="btn-primary ms-2"
|
||||
OnClick="Send">
|
||||
</WButton>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-column flex-lg-row-auto w-100 mw-lg-300px mw-xxl-350px">
|
||||
<div class="card p-10 mb-15 pb-8">
|
||||
<h2 class="text-dark fw-bold mb-2">
|
||||
<TL>User information</TL>
|
||||
</h2>
|
||||
|
||||
<div class="d-flex align-items-center mb-1">
|
||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
||||
<TL>Name</TL>: @(User.FirstName) @User.LastName
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
||||
<TL>Email</TL>: <a href="/admin/users/view/@User.Id">@(User.Email)</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="align-items-center mt-3">
|
||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Close ticket"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Closing"))"
|
||||
CssClasses="btn-danger float-end"
|
||||
OnClick="CloseTicket">
|
||||
</WButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private User? User;
|
||||
|
||||
private List<SupportChatMessage> Messages = new();
|
||||
private string[] Typing = Array.Empty<string>();
|
||||
|
||||
private string Content = "";
|
||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||
|
||||
private SmartFileSelect SmartFileSelect;
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
{
|
||||
User = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == Id);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
AdminService.OnMessage += OnMessage;
|
||||
AdminService.OnTypingChanged += OnTypingChanged;
|
||||
|
||||
await AdminService.Start(User);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMessages(LazyLoader arg)
|
||||
{
|
||||
Messages = (await AdminService.GetMessages()).ToList();
|
||||
}
|
||||
|
||||
private async Task OnTypingChanged(string[] typing)
|
||||
{
|
||||
Typing = typing;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnMessage(SupportChatMessage arg)
|
||||
{
|
||||
Messages.Insert(0, arg);
|
||||
|
||||
//TODO: Sound when message from system or admin
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
||||
Content = "File upload";
|
||||
|
||||
if (string.IsNullOrEmpty(Content))
|
||||
return;
|
||||
|
||||
var message = await AdminService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
||||
Content = "";
|
||||
|
||||
await SmartFileSelect.RemoveSelection();
|
||||
|
||||
Messages.Insert(0, message);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CloseTicket()
|
||||
{
|
||||
await AdminService.Close();
|
||||
}
|
||||
|
||||
private async Task OnTyping()
|
||||
{
|
||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||
{
|
||||
LastTypingTimestamp = DateTime.UtcNow;
|
||||
|
||||
await AdminService.SendTyping();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AdminService?.Dispose();
|
||||
}
|
||||
}
|
||||
138
Moonlight/Shared/Views/Admin/Sys/Plugins.razor
Normal file
138
Moonlight/Shared/Views/Admin/Sys/Plugins.razor
Normal file
@@ -0,0 +1,138 @@
|
||||
@page "/admin/system/plugins"
|
||||
|
||||
@using Moonlight.Shared.Components.Navigations
|
||||
@using Moonlight.App.Services.Plugins
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Plugin
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Interop
|
||||
|
||||
@inject PluginStoreService PluginStoreService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject PluginService PluginService
|
||||
@inject ToastService ToastService
|
||||
@inject ModalService ModalService
|
||||
|
||||
@attribute [PermissionRequired(nameof(Permissions.AdminSysPlugins))]
|
||||
|
||||
<AdminSystemNavigation Index="10"/>
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<TL>Installed plugins</TL>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<Table TableItem="MoonlightPlugin" Items="PluginService.Plugins" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Filterable="true" Sortable="false"/>
|
||||
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Author"))" Field="@(x => x.Author)" Filterable="true" Sortable="false"/>
|
||||
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Version"))" Field="@(x => x.Version)" Filterable="true" Sortable="false"/>
|
||||
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Path"))" Field="@(x => x.Name)" Filterable="false" Sortable="false">
|
||||
<Template>
|
||||
@{
|
||||
var path = PluginService.PluginFiles[context];
|
||||
}
|
||||
|
||||
<span>@(path)</span>
|
||||
</Template>
|
||||
</Column>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<TL>Official plugins</TL>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="PluginsLazyLoader" Load="LoadOfficialPlugins">
|
||||
<div class="table-responsive">
|
||||
<Table TableItem="OfficialMoonlightPlugin" Items="PluginList" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||
<Column TableItem="OfficialMoonlightPlugin" Width="80%" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Filterable="true" Sortable="false"/>
|
||||
<Column TableItem="OfficialMoonlightPlugin" Width="10%" Title="" Field="@(x => x.Name)" Filterable="false" Sortable="false">
|
||||
<Template>
|
||||
<WButton Text="@(SmartTranslateService.Translate("Show readme"))"
|
||||
CssClasses="btn-secondary"
|
||||
OnClick="() => ShowOfficialPluginReadme(context)"/>
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="OfficialMoonlightPlugin" Width="10%" Title="" Field="@(x => x.Name)" Filterable="false" Sortable="false">
|
||||
<Template>
|
||||
@if (PluginService.PluginFiles.Values.Any(x =>
|
||||
Path.GetFileName(x).Replace(".dll", "") == context.Name))
|
||||
{
|
||||
<WButton Text="@(SmartTranslateService.Translate("Update"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Updating"))"
|
||||
CssClasses="btn-primary"
|
||||
OnClick="() => UpdateOfficialPlugin(context)"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<WButton Text="@(SmartTranslateService.Translate("Install"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Installing"))"
|
||||
CssClasses="btn-primary"
|
||||
OnClick="() => InstallOfficialPlugin(context)"/>
|
||||
}
|
||||
</Template>
|
||||
</Column>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||
</Table>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pluginReadme" class="modal" style="display: none">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<TL>Plugin readme</TL>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@((MarkupString)Markdig.Markdown.ToHtml(PluginReadme))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private LazyLoader PluginsLazyLoader;
|
||||
private OfficialMoonlightPlugin[] PluginList;
|
||||
private string PluginReadme = "";
|
||||
|
||||
private async Task LoadOfficialPlugins(LazyLoader lazyLoader)
|
||||
{
|
||||
PluginList = await PluginStoreService.GetPlugins();
|
||||
}
|
||||
|
||||
private async Task ShowOfficialPluginReadme(OfficialMoonlightPlugin plugin)
|
||||
{
|
||||
PluginReadme = await PluginStoreService.GetPluginReadme(plugin);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await ModalService.Show("pluginReadme");
|
||||
}
|
||||
|
||||
private async Task InstallOfficialPlugin(OfficialMoonlightPlugin plugin)
|
||||
{
|
||||
await PluginStoreService.InstallPlugin(plugin);
|
||||
await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin"));
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task UpdateOfficialPlugin(OfficialMoonlightPlugin plugin)
|
||||
{
|
||||
await PluginStoreService.InstallPlugin(plugin, true);
|
||||
await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin. You need to reboot to apply changes"));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Services
|
||||
|
||||
@inject UserRepository UserRepository
|
||||
@@ -39,7 +40,11 @@
|
||||
</Column>
|
||||
<Column TableItem="User" Title="@(SmartTranslateService.Translate("First name"))" Field="@(x => x.FirstName)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="User" Title="@(SmartTranslateService.Translate("Last name"))" Field="@(x => x.LastName)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="User" Title="@(SmartTranslateService.Translate("Created at"))" Field="@(x => x.CreatedAt)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="User" Title="@(SmartTranslateService.Translate("Created at"))" Field="@(x => x.CreatedAt)" Sortable="true" Filterable="true">
|
||||
<Template>
|
||||
<span>@(Formatter.FormatDate(context.CreatedAt))</span>
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="User" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<a href="/admin/users/edit/@(context.Id)/">
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
@using Moonlight.App.Services
|
||||
@using CloudFlare.Client.Enumerators
|
||||
@using Moonlight.App.Services.Interop
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject DomainRepository DomainRepository
|
||||
@inject DomainService DomainService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AlertService AlertService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (Domain == null)
|
||||
@@ -181,9 +183,6 @@
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public User? User { get; set; }
|
||||
|
||||
private Domain? Domain;
|
||||
private DnsRecord[] DnsRecords;
|
||||
private DnsRecord NewRecord = new()
|
||||
@@ -205,13 +204,13 @@
|
||||
if (Domain == null)
|
||||
return;
|
||||
|
||||
if (User == null)
|
||||
if (IdentityService.User == null)
|
||||
{
|
||||
Domain = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Domain.Owner.Id != User.Id && !User.Admin)
|
||||
if (Domain.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin)
|
||||
{
|
||||
Domain = null;
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
@using Moonlight.App.Repositories.Domains
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject DomainRepository DomainRepository
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (Domains.Any())
|
||||
@@ -49,11 +51,8 @@
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code {
|
||||
|
||||
[CascadingParameter]
|
||||
public User? User { get; set; }
|
||||
|
||||
@code
|
||||
{
|
||||
private Domain[] Domains { get; set; }
|
||||
|
||||
private Task Load(LazyLoader loader)
|
||||
@@ -62,7 +61,7 @@
|
||||
.Get()
|
||||
.Include(x => x.SharedDomain)
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Owner == User)
|
||||
.Where(x => x.Owner == IdentityService.User)
|
||||
.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Events
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Helpers.Wings
|
||||
@using Moonlight.App.Helpers.Wings.Enums
|
||||
@using Moonlight.App.Plugin.UI
|
||||
@using Moonlight.App.Plugin.UI.Servers
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Plugins
|
||||
@using Moonlight.App.Services.Sessions
|
||||
@using Moonlight.Shared.Components.Xterm
|
||||
@using Moonlight.Shared.Views.Server.Settings
|
||||
@using Newtonsoft.Json
|
||||
|
||||
@inject ImageRepository ImageRepository
|
||||
@@ -21,10 +26,11 @@
|
||||
@inject DynamicBackgroundService DynamicBackgroundService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject IdentityService IdentityService
|
||||
@inject PluginService PluginService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<LazyLoader Load="LoadData">
|
||||
<LazyLoader Load="Load">
|
||||
@if (CurrentServer == null)
|
||||
{
|
||||
<NotFoundAlert/>
|
||||
@@ -33,7 +39,7 @@
|
||||
{
|
||||
if (NodeOnline)
|
||||
{
|
||||
if (Console.ConsoleState == ConsoleState.Connected)
|
||||
if (Console != null && Console.ConsoleState == ConsoleState.Connected)
|
||||
{
|
||||
if (Console.ServerState == ServerState.Installing || CurrentServer.Installing)
|
||||
{
|
||||
@@ -75,47 +81,36 @@
|
||||
<CascadingValue Value="Console">
|
||||
<CascadingValue Value="CurrentServer">
|
||||
<CascadingValue Value="Tags">
|
||||
<CascadingValue Value="Context">
|
||||
<SmartRouter Route="@Route">
|
||||
<Route Path="/">
|
||||
<ServerNavigation Index="0">
|
||||
<ServerConsole/>
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
<Route Path="/files">
|
||||
<ServerNavigation Index="1">
|
||||
<ServerFiles/>
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
<Route Path="/backups">
|
||||
<ServerNavigation Index="2">
|
||||
<ServerBackups/>
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
<Route Path="/network">
|
||||
<ServerNavigation Index="3">
|
||||
<ServerNetwork/>
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
<Route Path="/addons">
|
||||
<ServerNavigation Index="4">
|
||||
<ServerAddons/>
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
<Route Path="/settings">
|
||||
<ServerNavigation Index="5">
|
||||
<ServerSettings/>
|
||||
@foreach (var tab in Context.Tabs)
|
||||
{
|
||||
<Route Path="@(tab.Route)">
|
||||
<ServerNavigation Route="@(tab.Route)">
|
||||
@(tab.Component)
|
||||
</ServerNavigation>
|
||||
</Route>
|
||||
}
|
||||
</SmartRouter>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex justify-content-center flex-center">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h1 class="card-title">
|
||||
<TL>Connecting</TL>
|
||||
</h1>
|
||||
<p class="card-text fs-4">
|
||||
<TL>Connecting to the servers console to stream logs and the current resource usage</TL>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -144,21 +139,19 @@
|
||||
[Parameter]
|
||||
public string ServerUuid { get; set; }
|
||||
|
||||
|
||||
|
||||
[Parameter]
|
||||
public string? Route { get; set; }
|
||||
|
||||
private WingsConsole? Console;
|
||||
private Server? CurrentServer;
|
||||
private Node Node;
|
||||
private bool NodeOnline = false;
|
||||
private Image Image;
|
||||
private NodeAllocation NodeAllocation;
|
||||
private string[] Tags;
|
||||
|
||||
private Terminal? InstallConsole;
|
||||
|
||||
private ServerPageContext Context;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Console = new();
|
||||
@@ -171,68 +164,61 @@
|
||||
|
||||
Console.OnMessage += async (_, s) =>
|
||||
{
|
||||
if (Console.ServerState == ServerState.Installing)
|
||||
{
|
||||
if (InstallConsole != null)
|
||||
if (Console.ServerState == ServerState.Installing && InstallConsole != null)
|
||||
{
|
||||
if (s.IsInternal)
|
||||
await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m");
|
||||
else
|
||||
await InstallConsole.WriteLine(s.Content);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadData(LazyLoader lazyLoader)
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Requesting server data");
|
||||
|
||||
try
|
||||
{
|
||||
var uuid = Guid.Parse(ServerUuid);
|
||||
if (!Guid.TryParse(ServerUuid, out var uuid))
|
||||
return;
|
||||
|
||||
CurrentServer = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Allocations)
|
||||
.Include(x => x.Image)
|
||||
.Include("Image.Variables")
|
||||
.Include(x => x.Node)
|
||||
.Include(x => x.Variables)
|
||||
.Include(x => x.MainAllocation)
|
||||
.Include(x => x.Owner)
|
||||
.First(x => x.Uuid == uuid);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (CurrentServer != null)
|
||||
{
|
||||
if (CurrentServer.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin)
|
||||
{
|
||||
CurrentServer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentServer != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(CurrentServer.Image.BackgroundImageUrl))
|
||||
await DynamicBackgroundService.Reset();
|
||||
else
|
||||
await DynamicBackgroundService.Change(CurrentServer.Image.BackgroundImageUrl);
|
||||
|
||||
await lazyLoader.SetText("Checking node online status");
|
||||
|
||||
NodeOnline = await ServerService.IsHostUp(CurrentServer);
|
||||
|
||||
if (NodeOnline)
|
||||
{
|
||||
await lazyLoader.SetText("Checking server variables");
|
||||
if (!NodeOnline)
|
||||
return;
|
||||
|
||||
var image = ImageRepository
|
||||
.Get()
|
||||
.Include(x => x.Variables)
|
||||
.First(x => x.Id == CurrentServer.Image.Id);
|
||||
await lazyLoader.SetText("Checking server variables");
|
||||
|
||||
// Live variable migration
|
||||
|
||||
foreach (var variable in image.Variables)
|
||||
foreach (var variable in CurrentServer.Image.Variables)
|
||||
{
|
||||
if (!CurrentServer.Variables.Any(x => x.Key == variable.Key))
|
||||
if (CurrentServer.Variables.All(x => x.Key != variable.Key))
|
||||
{
|
||||
CurrentServer.Variables.Add(new ServerVariable()
|
||||
{
|
||||
@@ -246,15 +232,86 @@
|
||||
|
||||
// Tags
|
||||
|
||||
await lazyLoader.SetText("Requesting tags");
|
||||
await lazyLoader.SetText("Reading tags");
|
||||
|
||||
Tags = JsonConvert.DeserializeObject<string[]>(image.TagsJson) ?? Array.Empty<string>();
|
||||
Image = image;
|
||||
Tags = JsonConvert.DeserializeObject<string[]>(CurrentServer.Image.TagsJson) ?? Array.Empty<string>();
|
||||
|
||||
// Build server pages and settings
|
||||
|
||||
Context = new ServerPageContext()
|
||||
{
|
||||
Server = CurrentServer,
|
||||
User = IdentityService.User,
|
||||
ImageTags = Tags
|
||||
};
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Name = "Console",
|
||||
Route = "/",
|
||||
Icon = "terminal",
|
||||
Component = ComponentHelper.FromType(typeof(ServerConsole))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Name = "Files",
|
||||
Route = "/files",
|
||||
Icon = "folder",
|
||||
Component = ComponentHelper.FromType(typeof(ServerFiles))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Name = "Backups",
|
||||
Route = "/backups",
|
||||
Icon = "box",
|
||||
Component = ComponentHelper.FromType(typeof(ServerBackups))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Name = "Network",
|
||||
Route = "/network",
|
||||
Icon = "wifi",
|
||||
Component = ComponentHelper.FromType(typeof(ServerNetwork))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Name = "Settings",
|
||||
Route = "/settings",
|
||||
Icon = "cog",
|
||||
Component = ComponentHelper.FromType(typeof(ServerSettings))
|
||||
});
|
||||
|
||||
// Add default settings
|
||||
|
||||
Context.Settings.Add(new()
|
||||
{
|
||||
Name = "Rename",
|
||||
Component = ComponentHelper.FromType(typeof(ServerRenameSetting))
|
||||
});
|
||||
|
||||
Context.Settings.Add(new()
|
||||
{
|
||||
Name = "Reset",
|
||||
Component = ComponentHelper.FromType(typeof(ServerResetSetting))
|
||||
});
|
||||
|
||||
Context.Settings.Add(new()
|
||||
{
|
||||
Name = "Delete",
|
||||
Component = ComponentHelper.FromType(typeof(ServerDeleteSetting))
|
||||
});
|
||||
|
||||
Context = await PluginService.BuildServerPage(Context);
|
||||
|
||||
await lazyLoader.SetText("Connecting to console");
|
||||
|
||||
await ReconnectConsole();
|
||||
|
||||
// Register event system
|
||||
|
||||
await Event.On<Server>($"server.{CurrentServer.Uuid}.installComplete", this, server =>
|
||||
{
|
||||
NavigationManager.NavigateTo(NavigationManager.Uri, true);
|
||||
@@ -268,12 +325,6 @@
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
if (string.IsNullOrEmpty(Image.BackgroundImageUrl))
|
||||
await DynamicBackgroundService.Reset();
|
||||
else
|
||||
await DynamicBackgroundService.Change(Image.BackgroundImageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Helpers.Wings
|
||||
@using Moonlight.App.Helpers.Wings.Enums
|
||||
@using Moonlight.App.Plugin.UI
|
||||
@using Moonlight.App.Plugin.UI.Servers
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject SmartTranslateService TranslationService
|
||||
@@ -26,22 +28,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 d-flex flex-column flex-end mb-1">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="w-100 nav-link btn btn-sm btn-success fw-bold px-4 me-1 @(Console.ServerState == ServerState.Offline ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Start">
|
||||
<div class="btn-group btn-group-md">
|
||||
<button class="nav-link btn btn-md btn-success fw-bold px-4 me-1 @(Console.ServerState == ServerState.Offline ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Start">
|
||||
<TL>Start</TL>
|
||||
</button>
|
||||
<button class="w-100 nav-link btn btn-sm btn-primary fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Restart">
|
||||
<button class="nav-link btn btn-md btn-primary fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Restart">
|
||||
<TL>Restart</TL>
|
||||
</button>
|
||||
@if (Console.ServerState == ServerState.Stopping)
|
||||
{
|
||||
<button class="w-100 nav-link btn btn-sm btn-danger fw-bold px-4 me-1" aria-selected="true" role="tab" @onclick="Kill">
|
||||
<button class="nav-link btn btn-md btn-danger fw-bold px-4 me-1" aria-selected="true" role="tab" @onclick="Kill">
|
||||
<TL>Kill</TL>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="w-100 nav-link btn btn-sm btn-danger fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running || Console.ServerState == ServerState.Starting ? "" : "disabled")"
|
||||
<button class="nav-link btn btn-md btn-danger fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running || Console.ServerState == ServerState.Starting ? "" : "disabled")"
|
||||
aria-selected="true" role="tab" @onclick="Stop">
|
||||
<TL>Stop</TL>
|
||||
</button>
|
||||
@@ -53,11 +55,11 @@
|
||||
<div class="row mt-3">
|
||||
<div class="card card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 text-nowrap py-2">
|
||||
<span class="fw-bold"><TL>Shared IP</TL>:</span>
|
||||
<span class="ms-1 text-muted @(IdentityService.User.StreamerMode ? "blur" : "")">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation?.Port ?? 0}")</span>
|
||||
</div>
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 py-2">
|
||||
<span class="fw-bold"><TL>Server ID</TL>:</span>
|
||||
<span class="ms-1 text-muted">
|
||||
@if (IdentityService.User.Admin)
|
||||
@@ -70,7 +72,7 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 py-2">
|
||||
<span class="fw-bold"><TL>Status</TL>:</span>
|
||||
<span class="ms-1 text-muted">
|
||||
@switch (Console.ServerState)
|
||||
@@ -104,15 +106,15 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 py-2">
|
||||
<span class="fw-bold"><TL>Cpu</TL>:</span>
|
||||
<span class="ms-1 text-muted">@(Math.Round(Console.Resource.CpuAbsolute / (CurrentServer.Cpu / 100f), 2))%</span>
|
||||
</div>
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 py-2">
|
||||
<span class="fw-bold"><TL>Memory</TL>:</span>
|
||||
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @(Formatter.FormatSize(Console.Resource.MemoryLimitBytes))</span>
|
||||
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @Math.Round(CurrentServer.Memory / 1024f, 2) GB</span>
|
||||
</div>
|
||||
<div class="col fs-5">
|
||||
<div class="col fs-5 py-2">
|
||||
<span class="fw-bold"><TL>Disk</TL>:</span>
|
||||
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB</span>
|
||||
</div>
|
||||
@@ -122,66 +124,19 @@
|
||||
<div class="mt-5 row">
|
||||
<div class="d-flex flex-column flex-md-row card card-body p-5">
|
||||
<ul class="nav nav-tabs nav-pills flex-row border-0 flex-md-column fs-6 pe-5 mb-5">
|
||||
@foreach (var tab in Context.Tabs)
|
||||
{
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/" class="nav-link w-100 btn btn-flex @(Index == 0 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-terminal bx-sm me-2"></i>
|
||||
<a href="/server/@(CurrentServer.Uuid + tab.Route)" class="nav-link w-100 btn btn-flex @(Route == tab.Route ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-@(tab.Icon) bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Console</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/files" class="nav-link w-100 btn btn-flex @(Index == 1 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-folder bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Files</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/backups" class="nav-link w-100 btn btn-flex @(Index == 2 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-box bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Backups</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/network" class="nav-link w-100 btn btn-flex @(Index == 3 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-wifi bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Network</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/addons" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-plug bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Addons</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100 me-0 mb-md-2">
|
||||
<a href="/server/@(CurrentServer.Uuid)/settings" class="nav-link w-100 btn btn-flex @(Index == 5 ? "active" : "") btn-active-light-primary">
|
||||
<i class="bx bx-cog bx-sm me-2"></i>
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fs-5">
|
||||
<TL>Settings</TL>
|
||||
<TL>@(tab.Name)</TL>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="tab-content w-100">
|
||||
<div class="tab-pane fade show active">
|
||||
@@ -198,16 +153,17 @@
|
||||
[CascadingParameter]
|
||||
public Server CurrentServer { get; set; }
|
||||
|
||||
|
||||
|
||||
[CascadingParameter]
|
||||
public WingsConsole Console { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public ServerPageContext Context { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Index { get; set; } = 0;
|
||||
public string Route { get; set; } = "/";
|
||||
|
||||
//TODO: NodeIpService which loads and caches raw ips for nodes (maybe)
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.Shared.Views.Server.Settings
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Moonlight.App.Plugin.UI.Servers
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
<div class="row">
|
||||
@foreach (var setting in Settings)
|
||||
@foreach (var setting in Context.Settings)
|
||||
{
|
||||
<div class="col-12 col-md-6 p-3">
|
||||
<div class="accordion" id="serverSetting@(setting.GetHashCode())">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="serverSetting-header@(setting.GetHashCode())">
|
||||
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#serverSetting-body@(setting.GetHashCode())" aria-expanded="false" aria-controls="serverSetting-body@(setting.GetHashCode())">
|
||||
<TL>@(setting.Key)</TL>
|
||||
<TL>@(setting.Name)</TL>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="serverSetting-body@(setting.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverSetting-header@(setting.GetHashCode())" data-bs-parent="#serverSetting">
|
||||
<div class="accordion-body">
|
||||
@(GetComponent(setting.Value))
|
||||
@(setting.Component)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,68 +21,9 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public Server CurrentServer { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
private Dictionary<string, Type> Settings = new();
|
||||
|
||||
private Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
if (Tags.Contains("paperversion"))
|
||||
Settings.Add("Paper version", typeof(PaperVersionSetting));
|
||||
|
||||
if (Tags.Contains("forgeversion"))
|
||||
Settings.Add("Forge version", typeof(ForgeVersionSetting));
|
||||
|
||||
if (Tags.Contains("fabricversion"))
|
||||
Settings.Add("Fabric version", typeof(FabricVersionSetting));
|
||||
|
||||
if (Tags.Contains("join2start"))
|
||||
Settings.Add("Join2Start", typeof(Join2StartSetting));
|
||||
|
||||
if (Tags.Contains("javascriptversion"))
|
||||
Settings.Add("Javascript version", typeof(JavascriptVersionSetting));
|
||||
|
||||
if (Tags.Contains("javascriptfile"))
|
||||
Settings.Add("Javascript file", typeof(JavascriptFileSetting));
|
||||
|
||||
if (Tags.Contains("pythonversion"))
|
||||
Settings.Add("Python version", typeof(PythonVersionSetting));
|
||||
|
||||
if (Tags.Contains("javaversion"))
|
||||
Settings.Add("Java version", typeof(JavaRuntimeVersionSetting));
|
||||
|
||||
if (Tags.Contains("dotnetversion"))
|
||||
Settings.Add("Dotnet version", typeof(DotnetVersionSetting));
|
||||
|
||||
if (Tags.Contains("pythonfile"))
|
||||
Settings.Add("Python file", typeof(PythonFileSetting));
|
||||
|
||||
if (Tags.Contains("javafile"))
|
||||
Settings.Add("Jar file", typeof(JavaFileSetting));
|
||||
|
||||
if (Tags.Contains("dotnetfile"))
|
||||
Settings.Add("Dll file", typeof(DotnetFileSetting));
|
||||
|
||||
Settings.Add("Rename", typeof(ServerRenameSetting));
|
||||
|
||||
Settings.Add("Reset", typeof(ServerResetSetting));
|
||||
|
||||
Settings.Add("Delete", typeof(ServerDeleteSetting));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private RenderFragment GetComponent(Type type) => builder =>
|
||||
{
|
||||
builder.OpenComponent(0, type);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
public ServerPageContext Context { get; set; }
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
@page "/support"
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
@using System.Text.RegularExpressions
|
||||
@using Moonlight.App.Services.Files
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject ResourceService ResourceService
|
||||
@inject SupportChatClientService ClientService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<LazyLoader Load="LoadMessages">
|
||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||
@foreach (var message in Messages.ToArray())
|
||||
{
|
||||
if (message.Sender == null || message.Sender.Id != IdentityService.User.Id)
|
||||
{
|
||||
<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 ">
|
||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
@if (message.Sender != null)
|
||||
{
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
<TL>System</TL>
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
<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">
|
||||
@if (message.Sender == null)
|
||||
{
|
||||
<TL>@(message.Content)</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
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 (message.Attachment != "")
|
||||
{
|
||||
<div class="mt-3">
|
||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||
{
|
||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||
</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>
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="symbol symbol-35px symbol-circle ">
|
||||
<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">
|
||||
@foreach (var line in message.Content.Split("\n"))
|
||||
{
|
||||
@(line)<br/>
|
||||
}
|
||||
|
||||
@if (message.Attachment != "")
|
||||
{
|
||||
<div class="mt-3">
|
||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||
{
|
||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<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 ">
|
||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
<span>
|
||||
<TL>System</TL>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||
<TL>Welcome to the support chat. Ask your question here and we will help you</TL>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@if (Typing.Any())
|
||||
{
|
||||
<span class="mb-5 fs-5 d-flex flex-row">
|
||||
<div class="wave me-1">
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px"></div>
|
||||
</div>
|
||||
@if (Typing.Length > 1)
|
||||
{
|
||||
<span>
|
||||
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@(Typing.First()) <TL>is typing</TL>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<table class="w-100">
|
||||
<tr>
|
||||
<td class="align-top">
|
||||
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
||||
</td>
|
||||
<td class="w-100">
|
||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
||||
</textarea>
|
||||
</td>
|
||||
<td class="align-top">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Send"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Sending"))"
|
||||
CssClasses="btn-primary ms-2"
|
||||
OnClick="Send">
|
||||
</WButton>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
private List<SupportChatMessage> Messages = new();
|
||||
private string[] Typing = Array.Empty<string>();
|
||||
|
||||
private string Content = "";
|
||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||
|
||||
private SmartFileSelect SmartFileSelect;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Starting chat client");
|
||||
|
||||
ClientService.OnMessage += OnMessage;
|
||||
ClientService.OnTypingChanged += OnTypingChanged;
|
||||
|
||||
await ClientService.Start();
|
||||
}
|
||||
|
||||
private async Task LoadMessages(LazyLoader arg)
|
||||
{
|
||||
Messages = (await ClientService.GetMessages()).ToList();
|
||||
}
|
||||
|
||||
private async Task OnTypingChanged(string[] typing)
|
||||
{
|
||||
Typing = typing;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnMessage(SupportChatMessage message)
|
||||
{
|
||||
Messages.Insert(0, message);
|
||||
|
||||
//TODO: Sound when message from system or admin
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
||||
Content = "File upload";
|
||||
|
||||
var message = await ClientService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
||||
Content = "";
|
||||
|
||||
await SmartFileSelect.RemoveSelection();
|
||||
|
||||
Messages.Insert(0, message);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async void OnTyping()
|
||||
{
|
||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||
{
|
||||
LastTypingTimestamp = DateTime.UtcNow;
|
||||
await ClientService.SendTyping();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ClientService?.Dispose();
|
||||
}
|
||||
|
||||
private void OnFileChange(InputFileChangeEventArgs obj)
|
||||
{
|
||||
}
|
||||
}
|
||||
468
Moonlight/Shared/Views/Support/Index.razor
Normal file
468
Moonlight/Shared/Views/Support/Index.razor
Normal file
@@ -0,0 +1,468 @@
|
||||
@page "/support"
|
||||
@page "/support/{Id:int}"
|
||||
|
||||
@using Moonlight.App.Services.Tickets
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Events
|
||||
@using Moonlight.App.Services.Files
|
||||
@using Moonlight.App.Services.Sessions
|
||||
@using Moonlight.Shared.Components.Tickets
|
||||
|
||||
@inject TicketClientService ClientService
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject Repository<WebSpace> WebSpaceRepository
|
||||
@inject Repository<Domain> DomainRepository
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject IdentityService IdentityService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ResourceService ResourceService
|
||||
@inject EventSystem EventSystem
|
||||
|
||||
<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">
|
||||
<div class="d-flex flex-stack d-flex justify-content-center mb-5">
|
||||
<a href="/support" class="btn btn-primary">
|
||||
<TL>Create new ticket</TL>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
@foreach (var ticket in Tickets)
|
||||
{
|
||||
<div class="d-flex flex-stack py-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="ms-5">
|
||||
<a href="/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 != Tickets.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 (ClientService.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">@(ClientService.Ticket.IssueTopic)</span>
|
||||
<div class="mb-0 lh-1">
|
||||
<span class="fs-6 fw-bold text-muted me-2">
|
||||
<TL>Status</TL>
|
||||
</span>
|
||||
@switch (ClientService.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">@(ClientService.Ticket.Status)</span>
|
||||
|
||||
<span class="fs-6 fw-bold text-muted me-2">
|
||||
<TL>Priority</TL>
|
||||
</span>
|
||||
@switch (ClientService.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">@(ClientService.Ticket.Priority)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-toolbar">
|
||||
<div class="me-n3">
|
||||
<button class="btn btn-sm btn-icon btn-active-light-primary" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
|
||||
<i class="ki-duotone ki-dots-square fs-2">
|
||||
<span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span>
|
||||
</i>
|
||||
</button>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-200px py-3" data-kt-menu="true">
|
||||
<div class="menu-item px-3">
|
||||
<div class="menu-content text-muted pb-2 px-3 fs-7 text-uppercase">
|
||||
Contacts
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="#" class="menu-link px-3" data-bs-toggle="modal" data-bs-target="#kt_modal_users_search">
|
||||
Add Contact
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="#" class="menu-link flex-stack px-3" data-bs-toggle="modal" data-bs-target="#kt_modal_invite_friends">
|
||||
Invite Contacts
|
||||
<span class="ms-2" data-bs-toggle="tooltip" aria-label="Specify a contact email to send an invitation" data-bs-original-title="Specify a contact email to send an invitation" data-kt-initialized="1">
|
||||
<i class="ki-duotone ki-information fs-7">
|
||||
<span class="path1"></span><span class="path2"></span><span class="path3"></span>
|
||||
</i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3" data-kt-menu-trigger="hover" data-kt-menu-placement="right-start">
|
||||
<a href="#" class="menu-link px-3">
|
||||
<span class="menu-title">Groups</span>
|
||||
<span class="menu-arrow"></span>
|
||||
</a>
|
||||
<div class="menu-sub menu-sub-dropdown w-175px py-4">
|
||||
|
||||
<div class="menu-item px-3">
|
||||
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
|
||||
Create Group
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
|
||||
Invite Members
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-3 my-1">
|
||||
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<TL>Create a new ticket</TL>
|
||||
</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 (ClientService.Ticket == null)
|
||||
{
|
||||
<LazyLoader Load="LoadTicketCreate">
|
||||
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
|
||||
<div class="mb-3">
|
||||
<InputText @bind-Value="Model.IssueTopic"
|
||||
placeholder="@(SmartTranslateService.Translate("Enter a title for your ticket"))"
|
||||
class="form-control">
|
||||
</InputText>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<InputTextArea @bind-Value="Model.IssueDescription"
|
||||
placeholder="@(SmartTranslateService.Translate("Describe the issue you are experiencing"))"
|
||||
class="form-control">
|
||||
</InputTextArea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<InputTextArea @bind-Value="Model.IssueTries"
|
||||
placeholder="@(SmartTranslateService.Translate("Describe what you have tried to solve this issue"))"
|
||||
class="form-control">
|
||||
</InputTextArea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<select @bind="Model.Subject" class="form-select">
|
||||
@foreach (var subject in (TicketSubject[])Enum.GetValues(typeof(TicketSubject)))
|
||||
{
|
||||
if (Model.Subject == subject)
|
||||
{
|
||||
<option value="@(subject)" selected="">@(subject)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(subject)">@(subject)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@if (Model.Subject == TicketSubject.Domain)
|
||||
{
|
||||
<select @bind="Model.SubjectId" class="form-select">
|
||||
@foreach (var domain in Domains)
|
||||
{
|
||||
if (Model.SubjectId == domain.Id)
|
||||
{
|
||||
<option value="@(domain.Id)" selected="">@(domain.Name).@(domain.SharedDomain.Name)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(domain.Id)">@(domain.Name).@(domain.SharedDomain.Name)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
}
|
||||
else if (Model.Subject == TicketSubject.Server)
|
||||
{
|
||||
<select @bind="Model.SubjectId" class="form-select">
|
||||
@foreach (var server in Servers)
|
||||
{
|
||||
if (Model.SubjectId == server.Id)
|
||||
{
|
||||
<option value="@(server.Id)" selected="">@(server.Name)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(server.Id)">@(server.Name)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
}
|
||||
else if (Model.Subject == TicketSubject.Webspace)
|
||||
{
|
||||
<select @bind="Model.SubjectId" class="form-select">
|
||||
@foreach (var webSpace in WebSpaces)
|
||||
{
|
||||
if (Model.SubjectId == webSpace.Id)
|
||||
{
|
||||
<option value="@(webSpace.Id)" selected="">@(webSpace.Domain)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(webSpace.Id)">@(webSpace.Domain)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<TL>Create ticket</TL>
|
||||
</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</LazyLoader>
|
||||
}
|
||||
else
|
||||
{
|
||||
<TicketMessageView Messages="Messages"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (ClientService.Ticket != null)
|
||||
{
|
||||
<div class="card-footer pt-4">
|
||||
<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?> Tickets;
|
||||
private List<TicketMessage> Messages = new();
|
||||
private CreateTicketDataModel Model = new();
|
||||
private string MessageText;
|
||||
private SmartFileSelect FileSelect;
|
||||
|
||||
private Server[] Servers;
|
||||
private WebSpace[] WebSpaces;
|
||||
private Domain[] Domains;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await Unsubscribe();
|
||||
await ReloadTickets();
|
||||
await Subscribe();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task LoadTicketCreate(LazyLoader _)
|
||||
{
|
||||
Servers = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||
.ToArray();
|
||||
|
||||
WebSpaces = WebSpaceRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||
.ToArray();
|
||||
|
||||
Domains = DomainRepository
|
||||
.Get()
|
||||
.Include(x => x.SharedDomain)
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||
.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
var ticket = await ClientService.Create(
|
||||
Model.IssueTopic,
|
||||
Model.IssueDescription,
|
||||
Model.IssueTries,
|
||||
Model.Subject,
|
||||
Model.SubjectId
|
||||
);
|
||||
|
||||
Model = new();
|
||||
|
||||
NavigationManager.NavigateTo("/support/" + ticket.Id);
|
||||
}
|
||||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null)
|
||||
MessageText = "File upload";
|
||||
|
||||
if(string.IsNullOrEmpty(MessageText))
|
||||
return;
|
||||
|
||||
var msg = await ClientService.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 ticket =>
|
||||
{
|
||||
if (ticket.CreatedBy != null && ticket.CreatedBy.Id != IdentityService.User.Id)
|
||||
return;
|
||||
|
||||
await ReloadTickets(false);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
if (ClientService.Ticket != null)
|
||||
{
|
||||
await EventSystem.On<TicketMessage>($"tickets.{ClientService.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.{ClientService.Ticket.Id}.status", this, async _ =>
|
||||
{
|
||||
await ReloadTickets(false);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Unsubscribe()
|
||||
{
|
||||
await EventSystem.Off("tickets.new", this);
|
||||
|
||||
if (ClientService.Ticket != null)
|
||||
{
|
||||
await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.message", this);
|
||||
await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.status", this);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadTickets(bool reloadMessages = true)
|
||||
{
|
||||
ClientService.Ticket = null;
|
||||
Tickets = await ClientService.Get();
|
||||
|
||||
if (Id != 0)
|
||||
{
|
||||
ClientService.Ticket = Tickets
|
||||
.FirstOrDefault(x => x.Key.Id == Id)
|
||||
.Key ?? null;
|
||||
|
||||
if (ClientService.Ticket == null)
|
||||
return;
|
||||
|
||||
if (reloadMessages)
|
||||
{
|
||||
var msgs = await ClientService.GetMessages();
|
||||
Messages = msgs.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,14 @@
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.Shared.Components.WebsiteControl
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Plugin.UI.Webspaces
|
||||
@using Moonlight.App.Services.Plugins
|
||||
@using Moonlight.App.Services.Sessions
|
||||
|
||||
@inject Repository<WebSpace> WebSpaceRepository
|
||||
@inject WebSpaceService WebSpaceService
|
||||
@inject PluginService PluginService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@@ -32,43 +36,17 @@
|
||||
if (HostOnline)
|
||||
{
|
||||
<CascadingValue Value="CurrentWebspace">
|
||||
@{
|
||||
var index = 0;
|
||||
|
||||
switch (Route)
|
||||
<CascadingValue Value="Context">
|
||||
<SmartRouter Route="@(Route)">
|
||||
@foreach (var tab in Context.Tabs)
|
||||
{
|
||||
case "files":
|
||||
index = 1;
|
||||
break;
|
||||
case "sftp":
|
||||
index = 2;
|
||||
break;
|
||||
case "databases":
|
||||
index = 3;
|
||||
break;
|
||||
default:
|
||||
index = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
<WebSpaceNavigation Index="index" WebSpace="CurrentWebspace" />
|
||||
|
||||
@switch (Route)
|
||||
{
|
||||
case "files":
|
||||
<WebSpaceFiles />
|
||||
break;
|
||||
case "sftp":
|
||||
<WebSpaceSftp />
|
||||
break;
|
||||
case "databases":
|
||||
<WebSpaceDatabases />
|
||||
break;
|
||||
default:
|
||||
<WebSpaceDashboard />
|
||||
break;
|
||||
}
|
||||
<Route Path="@(tab.Route)">
|
||||
<WebSpaceNavigation Route="@(tab.Route)" WebSpace="CurrentWebspace"/>
|
||||
@(tab.Component)
|
||||
</Route>
|
||||
}
|
||||
</SmartRouter>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
}
|
||||
else
|
||||
@@ -101,6 +79,8 @@
|
||||
private WebSpace? CurrentWebspace;
|
||||
private bool HostOnline = false;
|
||||
|
||||
private WebspacePageContext Context;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
CurrentWebspace = WebSpaceRepository
|
||||
@@ -112,14 +92,53 @@
|
||||
if (CurrentWebspace != null)
|
||||
{
|
||||
if (CurrentWebspace.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin)
|
||||
{
|
||||
CurrentWebspace = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentWebspace != null)
|
||||
{
|
||||
await lazyLoader.SetText("Checking host system online status");
|
||||
|
||||
HostOnline = await WebSpaceService.IsHostUp(CurrentWebspace);
|
||||
|
||||
if (!HostOnline)
|
||||
return;
|
||||
|
||||
Context = new WebspacePageContext()
|
||||
{
|
||||
WebSpace = CurrentWebspace,
|
||||
User = IdentityService.User
|
||||
};
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Route = "/",
|
||||
Name = "Dashboard",
|
||||
Component = ComponentHelper.FromType(typeof(WebSpaceDashboard))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Route = "/files",
|
||||
Name = "Files",
|
||||
Component = ComponentHelper.FromType(typeof(WebSpaceFiles))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Route = "/sftp",
|
||||
Name = "SFTP",
|
||||
Component = ComponentHelper.FromType(typeof(WebSpaceSftp))
|
||||
});
|
||||
|
||||
Context.Tabs.Add(new()
|
||||
{
|
||||
Route = "/databases",
|
||||
Name = "Databases",
|
||||
Component = ComponentHelper.FromType(typeof(WebSpaceDatabases))
|
||||
});
|
||||
|
||||
Context = await PluginService.BuildWebspacePage(Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ Create a website;Eine Website erstellen
|
||||
Make your own websites with a webspace;Mit einem Webspace eine Website erstellen
|
||||
Create a domain;Eine Domain erstellen
|
||||
Make your servvices accessible throught your own domain;Mache deine Dienste mit einer Domain erreichbar
|
||||
Manage your websites;Deine Websiten verwalten
|
||||
Manage your websites;Deine Webseiten verwalten
|
||||
Modify the content of your websites;Den Inhalt deiner Websiten verwalten
|
||||
Manage your domains;Deine Domains verwalten
|
||||
Add, edit and delete dns records;DNS-Records hinzufügen, entfernen oder bearbeiten
|
||||
@@ -156,7 +156,7 @@ UuidIdentifier;UUIDIdentifier
|
||||
Override startup command;Startup Befehl überschreiben
|
||||
Loading;Wird geladen...
|
||||
Offline;Offline
|
||||
Connecting;Verbiden...
|
||||
Connecting;Verbinden...
|
||||
Start;Start
|
||||
Restart;Neustarten
|
||||
Stop;Stoppen
|
||||
|
||||
15
Moonlight/wwwroot/assets/js/moonlight.js
vendored
15
Moonlight/wwwroot/assets/js/moonlight.js
vendored
@@ -313,6 +313,21 @@
|
||||
'editor.background': '#000000'
|
||||
}
|
||||
});
|
||||
},
|
||||
checkConnection: async function(url, threshold) {
|
||||
const start = performance.now();
|
||||
|
||||
try
|
||||
{
|
||||
const response = await fetch(url, { mode: 'no-cors' });
|
||||
const latency = performance.now() - start;
|
||||
|
||||
if (latency > threshold)
|
||||
{
|
||||
moonlight.toasts.warning(`High latency detected: ${latency}ms. Moonlight might feel laggy. Please check your internet connection`);
|
||||
}
|
||||
}
|
||||
catch (error) {}
|
||||
}
|
||||
},
|
||||
flashbang: {
|
||||
|
||||
Reference in New Issue
Block a user