Added new file manager, renamed websites to webspaces. Added cloudpanel integration partialy. Added generic repos and more stuff

This commit is contained in:
Marcel Baumgartner 2023-04-19 21:04:40 +02:00
parent 8929c2793d
commit fd008e56aa
40 changed files with 3430 additions and 691 deletions

View file

@ -0,0 +1,83 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Plesk.Resources;
using Newtonsoft.Json;
using RestSharp;
namespace Moonlight.App.ApiClients.CloudPanel;
public class CloudPanelApiHelper
{
private readonly RestClient Client;
public CloudPanelApiHelper()
{
Client = new();
}
public async Task Post(Database.Entities.CloudPanel cloudPanel, string resource, object? body)
{
var request = CreateRequest(cloudPanel, resource);
request.Method = Method.Post;
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new CloudPanelException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
public async Task Delete(Database.Entities.CloudPanel cloudPanel, string resource, object? body)
{
var request = CreateRequest(cloudPanel, resource);
request.Method = Method.Delete;
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new CloudPanelException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
private RestRequest CreateRequest(Database.Entities.CloudPanel cloudPanel, string resource)
{
var url = $"{cloudPanel.ApiUrl}/" + resource;
var request = new RestRequest(url);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + cloudPanel.ApiKey);
return request;
}
}

View file

@ -0,0 +1,32 @@
using System.Runtime.Serialization;
namespace Moonlight.App.ApiClients.CloudPanel;
[Serializable]
public class CloudPanelException : Exception
{
public int StatusCode { get; set; }
public CloudPanelException()
{
}
public CloudPanelException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}
public CloudPanelException(string message) : base(message)
{
}
public CloudPanelException(string message, Exception inner) : base(message, inner)
{
}
protected CloudPanelException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}

View file

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.CloudPanel.Requests;
public class AddPhpSite
{
[JsonProperty("domainName")] public string DomainName { get; set; } = "";
[JsonProperty("siteUser")] public string SiteUser { get; set; } = "";
[JsonProperty("siteUserPassword")] public string SiteUserPassword { get; set; } = "";
[JsonProperty("vHostTemplate")] public string VHostTemplate { get; set; } = "";
[JsonProperty("phpVersion")] public string PhpVersion { get; set; } = "";
}

View file

@ -43,6 +43,10 @@ public class DataContext : DbContext
public DbSet<Website> Websites { get; set; }
public DbSet<StatisticsData> Statistics { get; set; }
public DbSet<NewsEntry> NewsEntries { get; set; }
public DbSet<CloudPanel> CloudPanels { get; set; }
public DbSet<MySqlDatabase> Databases { get; set; }
public DbSet<WebSpace> WebSpaces { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View file

@ -0,0 +1,10 @@
namespace Moonlight.App.Database.Entities;
public class CloudPanel
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string ApiUrl { get; set; } = "";
public string ApiKey { get; set; } = "";
public string Host { get; set; } = "";
}

View file

@ -0,0 +1,9 @@
namespace Moonlight.App.Database.Entities;
public class MySqlDatabase
{
public int Id { get; set; }
public WebSpace WebSpace { get; set; }
public string UserName { get; set; } = "";
public string Password { get; set; } = "";
}

View file

@ -0,0 +1,13 @@
namespace Moonlight.App.Database.Entities;
public class WebSpace
{
public int Id { get; set; }
public string Domain { get; set; } = "";
public string UserName { get; set; } = "";
public string Password { get; set; } = "";
public string VHostTemplate { get; set; } = "";
public User Owner { get; set; }
public List<MySqlDatabase> Databases { get; set; } = new();
public CloudPanel CloudPanel { get; set; }
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,121 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedCloudPanelModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CloudPanels",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ApiUrl = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ApiKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_CloudPanels", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "WebSpaces",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Domain = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
UserName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
VHostTemplate = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
OwnerId = table.Column<int>(type: "int", nullable: false),
CloudPanelId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WebSpaces", x => x.Id);
table.ForeignKey(
name: "FK_WebSpaces_CloudPanels_CloudPanelId",
column: x => x.CloudPanelId,
principalTable: "CloudPanels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WebSpaces_Users_OwnerId",
column: x => x.OwnerId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Databases",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
WebSpaceId = table.Column<int>(type: "int", nullable: false),
UserName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Databases", x => x.Id);
table.ForeignKey(
name: "FK_Databases_WebSpaces_WebSpaceId",
column: x => x.WebSpaceId,
principalTable: "WebSpaces",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Databases_WebSpaceId",
table: "Databases",
column: "WebSpaceId");
migrationBuilder.CreateIndex(
name: "IX_WebSpaces_CloudPanelId",
table: "WebSpaces",
column: "CloudPanelId");
migrationBuilder.CreateIndex(
name: "IX_WebSpaces_OwnerId",
table: "WebSpaces",
column: "OwnerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Databases");
migrationBuilder.DropTable(
name: "WebSpaces");
migrationBuilder.DropTable(
name: "CloudPanels");
}
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -19,6 +19,33 @@ namespace Moonlight.App.Database.Migrations
.HasAnnotation("ProductVersion", "7.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ApiKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ApiUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Host")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("CloudPanels");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{
b.Property<int>("Id")
@ -281,6 +308,30 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("SecurityLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserName")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("WebSpaceId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("WebSpaceId");
b.ToTable("Databases");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b =>
{
b.Property<int>("Id")
@ -748,6 +799,43 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("CloudPanelId")
.HasColumnType("int");
b.Property<string>("Domain")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("VHostTemplate")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("CloudPanelId");
b.HasIndex("OwnerId");
b.ToTable("WebSpaces");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
{
b.Property<int>("Id")
@ -828,6 +916,17 @@ namespace Moonlight.App.Database.Migrations
.HasForeignKey("ImageId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b =>
{
b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace")
.WithMany("Databases")
.HasForeignKey("WebSpaceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WebSpace");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Node", null)
@ -932,6 +1031,25 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("CurrentSubscription");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
{
b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel")
.WithMany()
.HasForeignKey("CloudPanelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CloudPanel");
b.Navigation("Owner");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
@ -971,6 +1089,11 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("Variables");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
{
b.Navigation("Databases");
});
#pragma warning restore 612, 618
}
}

View file

@ -0,0 +1,206 @@
using Logging.Net;
using Renci.SshNet;
using ConnectionInfo = Renci.SshNet.ConnectionInfo;
namespace Moonlight.App.Helpers.Files;
public class SftpFileAccess : FileAccess
{
private readonly string SftpHost;
private readonly string SftpUser;
private readonly string SftpPassword;
private readonly int SftpPort;
private readonly bool ForceUserDir;
private readonly SftpClient Client;
private string InternalPath
{
get
{
if (ForceUserDir)
return $"/home/{SftpUser}{CurrentPath}";
return InternalPath;
}
}
public SftpFileAccess(string sftpHost, string sftpUser, string sftpPassword, int sftpPort,
bool forceUserDir = false)
{
SftpHost = sftpHost;
SftpUser = sftpUser;
SftpPassword = sftpPassword;
SftpPort = sftpPort;
ForceUserDir = forceUserDir;
Client = new(
new ConnectionInfo(
SftpHost,
SftpPort,
SftpUser,
new PasswordAuthenticationMethod(
SftpUser,
SftpPassword
)
)
);
}
private void EnsureConnect()
{
if (!Client.IsConnected)
Client.Connect();
}
public override Task<FileData[]> Ls()
{
EnsureConnect();
var x = new List<FileData>();
foreach (var file in Client.ListDirectory(InternalPath))
{
if (file.Name != "." && file.Name != "..")
{
x.Add(new()
{
Name = file.Name,
Size = file.Attributes.Size,
IsFile = !file.IsDirectory
});
}
}
return Task.FromResult(x.ToArray());
}
public override Task Cd(string dir)
{
var x = Path.Combine(CurrentPath, dir).Replace("\\", "/") + "/";
x = x.Replace("//", "/");
CurrentPath = x;
return Task.CompletedTask;
}
public override Task Up()
{
CurrentPath = Path.GetFullPath(Path.Combine(CurrentPath, "..")).Replace("\\", "/").Replace("C:", "");
return Task.CompletedTask;
}
public override Task SetDir(string dir)
{
CurrentPath = dir;
return Task.CompletedTask;
}
public override Task<string> Read(FileData fileData)
{
EnsureConnect();
var textStream = Client.Open(InternalPath.TrimEnd('/') + "/" + fileData.Name, FileMode.Open);
if (textStream == null)
return Task.FromResult("");
var streamReader = new StreamReader(textStream);
var text = streamReader.ReadToEnd();
streamReader.Close();
textStream.Close();
return Task.FromResult(text);
}
public override Task Write(FileData fileData, string content)
{
EnsureConnect();
var textStream = Client.Open(InternalPath.TrimEnd('/') + "/" + fileData.Name, FileMode.Create);
var streamWriter = new StreamWriter(textStream);
streamWriter.Write(content);
streamWriter.Flush();
textStream.Flush();
streamWriter.Close();
textStream.Close();
return Task.CompletedTask;
}
public override async Task Upload(string name, Stream stream, Action<int>? progressUpdated = null)
{
var dataStream = new SyncStreamAdapter(stream);
await Task.Factory.FromAsync((x, _) => Client.BeginUploadFile(dataStream, InternalPath + name, x, null, u =>
{
progressUpdated?.Invoke((int)((long)u / stream.Length));
}),
Client.EndUploadFile, null);
}
public override Task MkDir(string name)
{
Client.CreateDirectory(InternalPath + name);
return Task.CompletedTask;
}
public override Task<string> Pwd()
{
return Task.FromResult(CurrentPath);
}
public override Task<string> DownloadUrl(FileData fileData)
{
throw new NotImplementedException();
}
public override Task<Stream> DownloadStream(FileData fileData)
{
var stream = new MemoryStream(100 * 1024 * 1024);
Client.DownloadFile(InternalPath + fileData.Name, stream);
return Task.FromResult<Stream>(stream);
}
public override Task Delete(FileData fileData)
{
Client.Delete(InternalPath + fileData.Name);
return Task.CompletedTask;
}
public override Task Move(FileData fileData, string newPath)
{
Client.RenameFile(InternalPath + fileData.Name, InternalPath + newPath);
return Task.CompletedTask;
}
public override Task Compress(params FileData[] files)
{
throw new NotImplementedException();
}
public override Task Decompress(FileData fileData)
{
throw new NotImplementedException();
}
public override Task<string> GetLaunchUrl()
{
return Task.FromResult($"sftp://{SftpUser}@{SftpHost}:{SftpPort}");
}
public override object Clone()
{
return new SftpFileAccess(SftpHost, SftpUser, SftpPassword, SftpPort, ForceUserDir);
}
}

View file

@ -0,0 +1,58 @@
namespace Moonlight.App.Helpers;
public class SyncStreamAdapter : Stream
{
private readonly Stream _stream;
public SyncStreamAdapter(Stream stream)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
public override bool CanRead => _stream.CanRead;
public override bool CanSeek => _stream.CanSeek;
public override bool CanWrite => _stream.CanWrite;
public override long Length => _stream.Length;
public override long Position
{
get => _stream.Position;
set => _stream.Position = value;
}
public override void Flush()
{
_stream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
var task = Task.Run(() => _stream.ReadAsync(buffer, offset, count));
return task.GetAwaiter().GetResult();
}
public override long Seek(long offset, SeekOrigin origin)
{
return _stream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_stream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
var task = Task.Run(() => _stream.WriteAsync(buffer, offset, count));
task.GetAwaiter().GetResult();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_stream?.Dispose();
}
base.Dispose(disposing);
}
}

View file

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class CloudPanelDataModel
{
[Required(ErrorMessage = "You have to enter a name")]
[MaxLength(32, ErrorMessage = "The name should not be longer than 32 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "You need to specify the host")]
public string Host { get; set; }
[Required(ErrorMessage = "You need to enter an api url")]
public string ApiUrl { get; set; }
[Required(ErrorMessage = "You need to enter an api key")]
public string ApiKey { get; set; }
}

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
namespace Moonlight.App.Repositories;
public class Repository<TEntity> where TEntity : class
{
private readonly DataContext DataContext;
private readonly DbSet<TEntity> DbSet;
public Repository(DataContext dbContext)
{
DataContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
DbSet = DataContext.Set<TEntity>();
}
public DbSet<TEntity> Get()
{
return DbSet;
}
public TEntity Add(TEntity entity)
{
var x = DbSet.Add(entity);
DataContext.SaveChanges();
return x.Entity;
}
public void Update(TEntity entity)
{
DbSet.Update(entity);
DataContext.SaveChanges();
}
public void Delete(TEntity entity)
{
DbSet.Remove(entity);
DataContext.SaveChanges();
}
}

View file

@ -7,17 +7,17 @@ public class SmartDeployService
{
private readonly NodeRepository NodeRepository;
private readonly PleskServerRepository PleskServerRepository;
private readonly WebsiteService WebsiteService;
private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService;
public SmartDeployService(
NodeRepository nodeRepository,
NodeService nodeService, PleskServerRepository pleskServerRepository, WebsiteService websiteService)
NodeService nodeService, PleskServerRepository pleskServerRepository, WebSpaceService webSpaceService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
PleskServerRepository = pleskServerRepository;
WebsiteService = websiteService;
WebSpaceService = webSpaceService;
}
public async Task<Node?> GetNode()
@ -44,10 +44,7 @@ public class SmartDeployService
foreach (var pleskServer in PleskServerRepository.Get().ToArray())
{
if (await WebsiteService.IsHostUp(pleskServer))
{
result.Add(pleskServer);
}
result.Add(pleskServer);
}
return result.FirstOrDefault();

View file

@ -9,7 +9,7 @@ public class StatisticsCaptureService
private readonly ConfigService ConfigService;
private readonly StatisticsRepository StatisticsRepository;
private readonly IServiceScopeFactory ServiceScopeFactory;
private readonly WebsiteService WebsiteService;
private readonly WebSpaceService WebSpaceService;
private readonly PleskServerRepository PleskServerRepository;
private PeriodicTimer Timer;
@ -21,7 +21,7 @@ public class StatisticsCaptureService
DataContext = provider.GetRequiredService<DataContext>();
ConfigService = configService;
StatisticsRepository = provider.GetRequiredService<StatisticsRepository>();
WebsiteService = provider.GetRequiredService<WebsiteService>();
WebSpaceService = provider.GetRequiredService<WebSpaceService>();
PleskServerRepository = provider.GetRequiredService<PleskServerRepository>();
var config = ConfigService.GetSection("Moonlight").GetSection("Statistics");
@ -48,7 +48,7 @@ public class StatisticsCaptureService
await foreach (var pleskServer in PleskServerRepository.Get())
{
databases += (await WebsiteService.GetDefaultDatabaseServer(pleskServer)).DbCount;
//databases += (await WebsiteService.GetDefaultDatabaseServer(pleskServer)).DbCount;
}
StatisticsRepository.Add("statistics.databasesCount", databases);

View file

@ -0,0 +1,166 @@
using Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.CloudPanel.Requests;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Files;
using Moonlight.App.Models.Plesk.Requests;
using Moonlight.App.Models.Plesk.Resources;
using Moonlight.App.Repositories;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
namespace Moonlight.App.Services;
public class WebSpaceService
{
private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly Repository<WebSpace> WebSpaceRepository;
private readonly CloudPanelApiHelper CloudPanelApiHelper;
public WebSpaceService(Repository<CloudPanel> cloudPanelRepository, Repository<WebSpace> webSpaceRepository, CloudPanelApiHelper cloudPanelApiHelper)
{
CloudPanelRepository = cloudPanelRepository;
WebSpaceRepository = webSpaceRepository;
CloudPanelApiHelper = cloudPanelApiHelper;
}
public async Task<WebSpace> Create(string domain, User owner, CloudPanel? ps = null)
{
if (WebSpaceRepository.Get().Any(x => x.Domain == domain))
throw new DisplayException("A website with this domain does already exist");
var cloudPanel = ps ?? CloudPanelRepository.Get().First();
var ftpLogin = domain.Replace(".", "_");
var ftpPassword = StringHelper.GenerateString(16);
var phpVersion = "8.1"; // TODO: Add config option or smth
var w = new WebSpace()
{
CloudPanel = cloudPanel,
Owner = owner,
Domain = domain,
UserName = ftpLogin,
Password = ftpPassword,
VHostTemplate = "Generic" //TODO: Implement as select option
};
var webSpace = WebSpaceRepository.Add(w);
try
{
await CloudPanelApiHelper.Post(cloudPanel, "site/php", new AddPhpSite()
{
VHostTemplate = w.VHostTemplate,
DomainName = w.Domain,
PhpVersion = phpVersion,
SiteUser = w.UserName,
SiteUserPassword = w.Password
});
}
catch (Exception)
{
WebSpaceRepository.Delete(webSpace);
throw;
}
return webSpace;
}
public async Task Delete(WebSpace w)
{
var website = EnsureData(w);
await CloudPanelApiHelper.Delete(website.CloudPanel, $"site/{website.Domain}", null);
WebSpaceRepository.Delete(website);
}
public async Task<bool> IsHostUp(CloudPanel cloudPanel)
{
try
{
//var res = await PleskApiHelper.Get<ServerStatus>(pleskServer, "server");
return true;
//if (res != null)
// return true;
}
catch (Exception e)
{
// ignored
}
return false;
}
public async Task<bool> IsHostUp(WebSpace w)
{
var webSpace = EnsureData(w);
return await IsHostUp(webSpace.CloudPanel);
}
#region SSL
public async Task<string[]> GetSslCertificates(WebSpace w)
{
var certs = new List<string>();
return certs.ToArray();
}
public async Task CreateSslCertificate(WebSpace w)
{
}
public async Task DeleteSslCertificate(WebSpace w, string name)
{
}
#endregion
#region Databases
public async Task<Models.Plesk.Resources.Database[]> GetDatabases(WebSpace w)
{
return Array.Empty<Models.Plesk.Resources.Database>();
}
public async Task CreateDatabase(WebSpace w, string name, string password)
{
}
public async Task DeleteDatabase(WebSpace w, Models.Plesk.Resources.Database database)
{
}
#endregion
public Task<FileAccess> CreateFileAccess(WebSpace w)
{
var webspace = EnsureData(w);
return Task.FromResult<FileAccess>(
new SftpFileAccess(webspace.CloudPanel.Host, webspace.UserName, webspace.Password, 22, true)
);
}
private WebSpace EnsureData(WebSpace webSpace)
{
if (webSpace.CloudPanel == null || webSpace.Owner == null)
return WebSpaceRepository
.Get()
.Include(x => x.CloudPanel)
.Include(x => x.Owner)
.First(x => x.Id == webSpace.Id);
return webSpace;
}
}

View file

@ -1,383 +0,0 @@
using Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Files;
using Moonlight.App.Models.Plesk.Requests;
using Moonlight.App.Models.Plesk.Resources;
using Moonlight.App.Repositories;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
namespace Moonlight.App.Services;
public class WebsiteService
{
private readonly WebsiteRepository WebsiteRepository;
private readonly PleskServerRepository PleskServerRepository;
private readonly PleskApiHelper PleskApiHelper;
private readonly UserRepository UserRepository;
public WebsiteService(WebsiteRepository websiteRepository, PleskApiHelper pleskApiHelper, PleskServerRepository pleskServerRepository, UserRepository userRepository)
{
WebsiteRepository = websiteRepository;
PleskApiHelper = pleskApiHelper;
PleskServerRepository = pleskServerRepository;
UserRepository = userRepository;
}
public async Task<Website> Create(string baseDomain, User owner, PleskServer? ps = null)
{
if (WebsiteRepository.Get().Any(x => x.BaseDomain == baseDomain))
throw new DisplayException("A website with this domain does already exist");
var pleskServer = ps ?? PleskServerRepository.Get().First();
var ftpLogin = baseDomain;
var ftpPassword = StringHelper.GenerateString(16);
var w = new Website()
{
PleskServer = pleskServer,
Owner = owner,
BaseDomain = baseDomain,
PleskId = 0,
FtpPassword = ftpPassword,
FtpLogin = ftpLogin
};
var website = WebsiteRepository.Add(w);
try
{
var id = await GetAdminAccount(pleskServer);
var result = await PleskApiHelper.Post<CreateResult>(pleskServer, "domains", new CreateDomain()
{
Description = $"moonlight website {website.Id}",
Name = baseDomain,
HostingType = "virtual",
Plan = new()
{
Name = "Unlimited"
},
HostingSettings = new()
{
FtpLogin = ftpLogin,
FtpPassword = ftpPassword
},
OwnerClient = new()
{
Id = id
}
});
website.PleskId = result.Id;
WebsiteRepository.Update(website);
}
catch (Exception e)
{
WebsiteRepository.Delete(website);
throw;
}
return website;
}
public async Task Delete(Website w)
{
var website = EnsureData(w);
await PleskApiHelper.Delete(website.PleskServer, $"domains/{w.PleskId}", null);
WebsiteRepository.Delete(website);
}
public async Task<bool> IsHostUp(PleskServer pleskServer)
{
try
{
var res = await PleskApiHelper.Get<ServerStatus>(pleskServer, "server");
if (res != null)
return true;
}
catch (Exception e)
{
// ignored
}
return false;
}
public async Task<bool> IsHostUp(Website w)
{
var website = EnsureData(w);
try
{
var res = await PleskApiHelper.Get<ServerStatus>(website.PleskServer, "server");
if (res != null)
return true;
}
catch (Exception)
{
// ignored
}
return false;
}
#region Get host
public async Task<string> GetHost(PleskServer pleskServer)
{
return (await PleskApiHelper.Get<ServerStatus>(pleskServer, "server")).Hostname;
}
public async Task<string> GetHost(Website w)
{
var website = EnsureData(w);
return await GetHost(website.PleskServer);
}
#endregion
private async Task<int> GetAdminAccount(PleskServer pleskServer)
{
var users = await PleskApiHelper.Get<Client[]>(pleskServer, "clients");
var user = users.FirstOrDefault(x => x.Type == "admin");
if (user == null)
throw new DisplayException("No admin account in plesk found");
return user.Id;
}
#region SSL
public async Task<string[]> GetSslCertificates(Website w)
{
var website = EnsureData(w);
var certs = new List<string>();
var data = await ExecuteCli(website.PleskServer, "certificate", p =>
{
p.Add("-l");
p.Add("-domain");
p.Add(w.BaseDomain);
});
string[] lines = data.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
if (line.Contains("Lets Encrypt"))
{
string[] parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length > 6)
certs.Add($"{parts[4]} {parts[5]} {parts[6]}");
}
else if (line.Contains("Listing of SSL/TLS certificates repository was successful"))
{
// This line indicates the end of the certificate listing, so we can stop parsing
break;
}
}
return certs.ToArray();
}
public async Task CreateSslCertificate(Website w)
{
var website = EnsureData(w);
await ExecuteCli(website.PleskServer, "extension", p =>
{
p.Add("--exec");
p.Add("letsencrypt");
p.Add("cli.php");
p.Add("-d");
p.Add(website.BaseDomain);
p.Add("-m");
p.Add(website.Owner.Email);
});
}
public async Task DeleteSslCertificate(Website w, string name)
{
var website = EnsureData(w);
try
{
await ExecuteCli(website.PleskServer, "site", p =>
{
p.Add("-u");
p.Add(website.BaseDomain);
p.Add("-ssl");
p.Add("false");
});
try
{
await ExecuteCli(website.PleskServer, "certificate", p =>
{
p.Add("--remove");
p.Add(name);
p.Add("-domain");
p.Add(website.BaseDomain);
});
}
catch (Exception e)
{
Logger.Warn("Error removing ssl certificate");
Logger.Warn(e);
throw new DisplayException("An unknown error occured while removing ssl certificate");
}
}
catch (DisplayException)
{
// Redirect all display exception to soft error handler
throw;
}
catch (Exception e)
{
Logger.Warn("Error disabling ssl certificate");
Logger.Warn(e);
throw new DisplayException("An unknown error occured while disabling ssl certificate");
}
}
#endregion
#region Databases
public async Task<Models.Plesk.Resources.Database[]> GetDatabases(Website w)
{
var website = EnsureData(w);
var dbs = await PleskApiHelper.Get<Models.Plesk.Resources.Database[]>(
website.PleskServer,
$"databases?domain={w.BaseDomain}"
);
return dbs;
}
public async Task CreateDatabase(Website w, string name, string password)
{
var website = EnsureData(w);
var server = await GetDefaultDatabaseServer(website);
if (server == null)
throw new DisplayException("No database server marked as default found");
var dbReq = new CreateDatabase()
{
Name = name,
Type = "mysql",
ParentDomain = new()
{
Name = website.BaseDomain
},
ServerId = server.Id
};
var db = await PleskApiHelper.Post<Models.Plesk.Resources.Database>(website.PleskServer, "databases", dbReq);
if (db == null)
throw new DisplayException("Unable to create database via api");
var dbUserReq = new CreateDatabaseUser()
{
DatabaseId = db.Id,
Login = name,
Password = password
};
await PleskApiHelper.Post(website.PleskServer, "dbusers", dbUserReq);
}
public async Task DeleteDatabase(Website w, Models.Plesk.Resources.Database database)
{
var website = EnsureData(w);
var dbUsers = await PleskApiHelper.Get<DatabaseUser[]>(
website.PleskServer,
$"dbusers?dbId={database.Id}"
);
foreach (var dbUser in dbUsers)
{
await PleskApiHelper.Delete(website.PleskServer, $"dbusers/{dbUser.Id}", null);
}
await PleskApiHelper.Delete(website.PleskServer, $"databases/{database.Id}", null);
}
public async Task<DatabaseServer?> GetDefaultDatabaseServer(PleskServer pleskServer)
{
var dbServers = await PleskApiHelper.Get<DatabaseServer[]>(pleskServer, "dbservers");
return dbServers.FirstOrDefault(x => x.IsDefault);
}
public async Task<DatabaseServer?> GetDefaultDatabaseServer(Website w)
{
var website = EnsureData(w);
return await GetDefaultDatabaseServer(website.PleskServer);
}
#endregion
public async Task<FileAccess> CreateFileAccess(Website w)
{
var website = EnsureData(w);
var host = await GetHost(website.PleskServer);
return new FtpFileAccess(host, 21, website.FtpLogin, website.FtpPassword);
}
private async Task<string> ExecuteCli(
PleskServer server,
string cli, Action<List<string>>? parameters = null,
Action<Dictionary<string, string>>? variables = null
)
{
var p = new List<string>();
var v = new Dictionary<string, string>();
parameters?.Invoke(p);
variables?.Invoke(v);
var req = new CliCall()
{
Env = v,
Params = p
};
var res = await PleskApiHelper.Post<CliResult>(server, $"cli/{cli}/call", req);
return res.Stdout;
}
private Website EnsureData(Website website)
{
if (website.PleskServer == null || website.Owner == null)
return WebsiteRepository
.Get()
.Include(x => x.PleskServer)
.Include(x => x.Owner)
.First(x => x.Id == website.Id);
return website;
}
}

View file

@ -41,6 +41,7 @@
<PackageReference Include="PteroConsole.NET" Version="1.0.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="XtermBlazor" Version="1.6.1" />
</ItemGroup>
@ -67,6 +68,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="App\ApiClients\CloudPanel\Resources\" />
<Folder Include="App\Http\Middleware" />
<Folder Include="App\Models\Daemon\Requests" />
<Folder Include="App\Models\Google\Resources" />

View file

@ -2,6 +2,7 @@ using BlazorDownloadFile;
using BlazorTable;
using CurrieTechnologies.Razor.SweetAlert2;
using Logging.Net;
using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.Database;
using Moonlight.App.Helpers;
using Moonlight.App.LogMigrator;
@ -76,6 +77,7 @@ namespace Moonlight
builder.Services.AddScoped<AuditLogEntryRepository>();
builder.Services.AddScoped<ErrorLogEntryRepository>();
builder.Services.AddScoped<SecurityLogEntryRepository>();
builder.Services.AddScoped(typeof(Repository<>));
// Services
builder.Services.AddSingleton<ConfigService>();
@ -102,7 +104,7 @@ namespace Moonlight
builder.Services.AddScoped<NotificationClientService>();
builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<SmartDeployService>();
builder.Services.AddScoped<WebsiteService>();
builder.Services.AddScoped<WebSpaceService>();
builder.Services.AddScoped<StatisticsViewService>();
builder.Services.AddScoped<GoogleOAuth2Service>();
@ -136,6 +138,7 @@ namespace Moonlight
builder.Services.AddSingleton<HostSystemHelper>();
builder.Services.AddScoped<DaemonApiHelper>();
builder.Services.AddScoped<PleskApiHelper>();
builder.Services.AddScoped<CloudPanelApiHelper>();
// Background services
builder.Services.AddSingleton<DiscordBotService>();

View file

@ -1,4 +1,5 @@
@using Moonlight.App.Helpers.Files
@using Logging.Net
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">

View file

@ -2,13 +2,13 @@
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/websites">
<TL>Websites</TL>
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/webspaces">
<TL>Webspaces</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/websites/servers">
<TL>Plesk servers</TL>
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/webspaces/servers">
<TL>Cloud panels</TL>
</a>
</li>
</ul>

View file

@ -1,14 +1,14 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@inject WebsiteService WebsiteService
@inject WebSpaceService WebSpaceService
@inject SmartTranslateService SmartTranslateService
<div class="row gy-5 g-xl-10">
<div class="col-xl-4 mb-xl-10">
<div class="card h-md-100">
<div class="card-body d-flex flex-column flex-center">
<img class="img-fluid" src="https://image.thum.io/get/http://@(CurrentWebsite.BaseDomain)" alt="Website screenshot"/>
<img class="img-fluid" src="https://image.thum.io/get/http://@(CurrentWebSpace.Domain)" alt="Website screenshot"/>
</div>
</div>
</div>
@ -85,7 +85,7 @@
@code
{
[CascadingParameter]
public Website CurrentWebsite { get; set; }
public WebSpace CurrentWebSpace { get; set; }
private string[] Certs;
@ -94,18 +94,18 @@
private async Task Load(LazyLoader lazyLoader)
{
await lazyLoader.SetText("Loading certificates");
Certs = await WebsiteService.GetSslCertificates(CurrentWebsite);
Certs = await WebSpaceService.GetSslCertificates(CurrentWebSpace);
}
private async Task CreateCertificate()
{
await WebsiteService.CreateSslCertificate(CurrentWebsite);
await WebSpaceService.CreateSslCertificate(CurrentWebSpace);
await LazyLoader.Reload();
}
private async Task DeleteCertificate(string name)
{
await WebsiteService.DeleteSslCertificate(CurrentWebsite, name);
await WebSpaceService.DeleteSslCertificate(CurrentWebSpace, name);
await LazyLoader.Reload();
}
}

View file

@ -4,7 +4,7 @@
@using Moonlight.App.Services
@inject SmartTranslateService SmartTranslateService
@inject WebsiteService WebsiteService
@inject WebSpaceService WebSpaceService
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="card w-100 mb-4">
@ -93,7 +93,7 @@
else
{
<div class="alert alert-warning">
<TL>No databases found for this website</TL>
<TL>No databases found for this webspace</TL>
</div>
}
</LazyLoader>
@ -101,7 +101,7 @@
@code
{
[CascadingParameter]
public Website CurrentWebsite { get; set; }
public WebSpace CurrentWebSpace { get; set; }
private LazyLoader LazyLoader;
private Database[] Databases;
@ -112,25 +112,19 @@
private async Task Load(LazyLoader arg)
{
Databases = await WebsiteService.GetDatabases(CurrentWebsite);
if (Databases.Any())
{
DatabaseServer = (await WebsiteService.GetDefaultDatabaseServer(CurrentWebsite))!;
Host = await WebsiteService.GetHost(CurrentWebsite);
}
Databases = await WebSpaceService.GetDatabases(CurrentWebSpace);
}
private async Task OnValidSubmit()
{
await WebsiteService.CreateDatabase(CurrentWebsite, Model.Name, Model.Password);
await WebSpaceService.CreateDatabase(CurrentWebSpace, Model.Name, Model.Password);
Model = new();
await LazyLoader.Reload();
}
private async Task DeleteDatabase(Database database)
{
await WebsiteService.DeleteDatabase(CurrentWebsite, database);
await WebSpaceService.DeleteDatabase(CurrentWebSpace, database);
await LazyLoader.Reload();
}
}

View file

@ -3,7 +3,7 @@
@using Moonlight.App.Services
@using Moonlight.Shared.Components.FileManagerPartials
@inject WebsiteService WebsiteService
@inject WebSpaceService WebSpaceService
<LazyLoader Load="Load">
<FileManager Access="Access">
@ -13,12 +13,12 @@
@code
{
[CascadingParameter]
public Website CurrentWebsite { get; set; }
public WebSpace CurrentWebSpace { get; set; }
private FileAccess Access;
private async Task Load(LazyLoader arg)
{
Access = await WebsiteService.CreateFileAccess(CurrentWebsite);
Access = await WebSpaceService.CreateFileAccess(CurrentWebSpace);
}
}

View file

@ -0,0 +1,55 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@inject WebSpaceService WebSpaceService
<div class="card card-flush h-xl-100">
<div class="card-body pt-2">
<div class="mt-7 row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Host</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(CurrentWebSpace.CloudPanel.Host)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Port</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="21">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Username</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(CurrentWebSpace.UserName)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Password</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled blur-unless-hover" disabled="disabled" value="@(CurrentWebSpace.Password)">
</div>
</div>
</div>
</div>
@code
{
[CascadingParameter]
public WebSpace CurrentWebSpace { get; set; }
}

View file

@ -10,8 +10,8 @@
</div>
</div>
<div class="d-flex flex-column">
<div class="mb-1 fs-4">@(Website.BaseDomain)</div>
<div class="text-muted fs-5">@(Website.PleskServer.Name)</div>
<div class="mb-1 fs-4">@(WebSpace.Domain)</div>
<div class="text-muted fs-5">@(WebSpace.CloudPanel.Name)</div>
</div>
</div>
</div>
@ -24,22 +24,22 @@
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/website/@(Website.Id)">
<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="/website/@(Website.Id)/files">
<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="/website/@(Website.Id)/ftp">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/webspace/@(WebSpace.Id)/ftp">
<TL>Ftp</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="/website/@(Website.Id)/databases">
<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>
</li>
@ -53,5 +53,5 @@
public int Index { get; set; }
[Parameter]
public Website Website { get; set; }
public WebSpace WebSpace { get; set; }
}

View file

@ -1,64 +0,0 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@inject WebsiteService WebsiteService
<div class="card card-flush h-xl-100">
<div class="card-body pt-2">
<LazyLoader Load="Load">
<div class="mt-7 row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Host</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(FtpHost)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Port</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="21">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Username</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Website.FtpLogin)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Ftp Password</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled blur-unless-hover" disabled="disabled" value="@(Website.FtpPassword)">
</div>
</div>
</LazyLoader>
</div>
</div>
@code
{
[CascadingParameter]
public Website Website { get; set; }
private string FtpHost = "N/A";
private async Task Load(LazyLoader arg)
{
FtpHost = await WebsiteService.GetHost(Website.PleskServer);
}
}

View file

@ -33,7 +33,7 @@
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/websites">
<a class="mt-4 card" href="/admin/webspaces">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">

View file

@ -1,98 +0,0 @@
@page "/admin/websites/servers/edit/{Id:int}"
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@inject PleskServerRepository PleskServerRepository
@inject NavigationManager NavigationManager
<OnlyAdmin>
<LazyLoader Load="Load">
@if (PleskServer == null)
{
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
<div class="card-body text-center">
<h1 class="card-title">
<TL>Plesk server not found</TL>
</h1>
<p class="card-text fs-4">
<TL>A plesk server with that id cannot be found</TL>
</p>
</div>
</div>
</div>
}
else
{
<div class="card card-body p-10">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<label class="form-label">
<TL>Name</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Api Url</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.ApiUrl" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Api Key</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.ApiKey" class="blur-unless-hover form-control"></InputText>
</div>
<div>
<button type="submit" class="btn btn-primary float-end">
<TL>Save</TL>
</button>
</div>
</SmartForm>
</div>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[Parameter]
public int Id { get; set; }
private PleskServer? PleskServer;
private PleskServerDataModel Model = new();
private Task OnValidSubmit()
{
PleskServer!.Name = Model.Name;
PleskServer.ApiUrl = Model.ApiUrl;
PleskServer.ApiKey = Model.ApiKey;
PleskServerRepository.Update(PleskServer);
NavigationManager.NavigateTo("/admin/websites/servers");
return Task.CompletedTask;
}
private Task Load(LazyLoader arg)
{
PleskServer = PleskServerRepository
.Get()
.FirstOrDefault(x => x.Id == Id);
if (PleskServer != null)
{
Model.Name = PleskServer.Name;
Model.ApiUrl = PleskServer.ApiUrl;
Model.ApiKey = PleskServer.ApiKey;
}
return Task.CompletedTask;
}
}

View file

@ -1,4 +1,4 @@
@page "/admin/websites/"
@page "/admin/webspaces/"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@ -8,53 +8,53 @@
@using BlazorTable
@inject SmartTranslateService SmartTranslateService
@inject WebsiteRepository WebsiteRepository
@inject WebsiteService WebsiteService
@inject Repository<WebSpace> WebSpaceRepository
@inject WebSpaceService WebSpaceService
<OnlyAdmin>
<AdminWebsitesNavigation Index="0"/>
<AdminWebspacesNavigation Index="0"/>
<div class="card">
<div class="card-header border-0 pt-5">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold fs-3 mb-1">
<TL>Websites</TL>
<TL>Webspaces</TL>
</span>
</h3>
<div class="card-toolbar">
<a href="/admin/websites/new" class="btn btn-sm btn-light-success">
<a href="/admin/webspaces/new" class="btn btn-sm btn-light-success">
<i class="bx bx-user-plus"></i>
<TL>New website</TL>
<TL>New webspace</TL>
</a>
</div>
</div>
<div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="table-responsive">
<Table TableItem="Website" Items="Websites" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Website" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Website" Title="@(SmartTranslateService.Translate("Base domain"))" Field="@(x => x.BaseDomain)" Sortable="true" Filterable="true">
<Table TableItem="WebSpace" Items="WebSpaces" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="WebSpace" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="WebSpace" Title="@(SmartTranslateService.Translate("Domain"))" Field="@(x => x.Domain)" Sortable="true" Filterable="true">
<Template>
<a href="/website/@(context.Id)/">
@(context.BaseDomain)
<a href="/webspace/@(context.Id)/">
@(context.Domain)
</a>
</Template>
</Column>
<Column TableItem="Website" Title="@(SmartTranslateService.Translate("Owner"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Column TableItem="WebSpace" Title="@(SmartTranslateService.Translate("Owner"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a href="/admin/users/view/@(context.Owner.Id)">
@(context.Owner.Email)
</a>
</Template>
</Column>
<Column TableItem="Website" Title="@(SmartTranslateService.Translate("Plesk server"))" Field="@(x => x.PleskServer.Id)" Sortable="true" Filterable="true">
<Column TableItem="WebSpace" Title="@(SmartTranslateService.Translate("Cloud panel"))" Field="@(x => x.CloudPanel.Name)" Sortable="true" Filterable="true">
<Template>
<a href="/admin/websites/servers/edit/@(context.PleskServer.Id)/">
@(context.PleskServer.Name)
<a href="/admin/webspaces/servers/edit/@(context.CloudPanel.Id)/">
@(context.CloudPanel.Name)
</a>
</Template>
</Column>
<Column TableItem="Website" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Column TableItem="WebSpace" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<DeleteButton Confirm="true" OnClick="() => Delete(context)">
</DeleteButton>
@ -72,22 +72,21 @@
{
private LazyLoader LazyLoader;
private Website[] Websites;
private WebSpace[] WebSpaces;
private Task Load(LazyLoader lazyLoader)
{
Websites = WebsiteRepository
WebSpaces = WebSpaceRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.PleskServer)
.Include(x => x.CloudPanel)
.ToArray();
return Task.CompletedTask;
}
private async Task Delete(Website website)
private async Task Delete(WebSpace webSpace)
{
await WebsiteService.Delete(website);
await WebSpaceService.Delete(webSpace);
await LazyLoader.Reload();
}
}

View file

@ -1,11 +1,11 @@
@page "/admin/websites/new"
@page "/admin/webspaces/new"
@using Moonlight.App.Models.Forms
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories
@inject WebsiteService WebsiteService
@inject WebSpaceService WebSpaceService
@inject UserRepository UserRepository
@inject NavigationManager NavigationManager
@ -46,9 +46,9 @@
private async Task OnValidSubmit()
{
await WebsiteService.Create(Model.BaseDomain, Model.User);
await WebSpaceService.Create(Model.BaseDomain, Model.User);
NavigationManager.NavigateTo("/admin/websites");
NavigationManager.NavigateTo("/admin/webspaces");
}
private Task Load(LazyLoader arg)

View file

@ -0,0 +1,102 @@
@page "/admin/webspaces/servers/edit/{Id:int}"
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Mappy.Net
@inject Repository<CloudPanel> CloudPanelRepository
@inject NavigationManager NavigationManager
<OnlyAdmin>
<LazyLoader Load="Load">
@if (CloudPanel == null)
{
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
<div class="card-body text-center">
<h1 class="card-title">
<TL>Cloud panel not found</TL>
</h1>
<p class="card-text fs-4">
<TL>A cloud panel with that id cannot be found</TL>
</p>
</div>
</div>
</div>
}
else
{
<div class="card card-body p-10">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<label class="form-label">
<TL>Name</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Host</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.Host" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Api Url</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.ApiUrl" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Api Key</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.ApiKey" class="blur-unless-hover form-control"></InputText>
</div>
<div>
<button type="submit" class="btn btn-primary float-end">
<TL>Save</TL>
</button>
</div>
</SmartForm>
</div>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[Parameter]
public int Id { get; set; }
private CloudPanel? CloudPanel;
private CloudPanelDataModel Model = new();
private Task OnValidSubmit()
{
// Apply changes by mapping values using the override feature
CloudPanel = Mapper.Map(CloudPanel!, Model);
CloudPanelRepository.Update(CloudPanel);
NavigationManager.NavigateTo("/admin/webspaces/servers");
return Task.CompletedTask;
}
private Task Load(LazyLoader arg)
{
CloudPanel = CloudPanelRepository
.Get()
.FirstOrDefault(x => x.Id == Id);
if (CloudPanel != null)
{
Model = Mapper.Map<CloudPanelDataModel>(CloudPanel);
}
return Task.CompletedTask;
}
}

View file

@ -1,4 +1,4 @@
@page "/admin/websites/servers"
@page "/admin/webspaces/servers"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@ -7,38 +7,39 @@
@using BlazorTable
@inject SmartTranslateService SmartTranslateService
@inject PleskServerRepository PleskServerRepository
@inject WebsiteService WebsiteService
@inject Repository<CloudPanel> CloudPanelRepository
@inject WebSpaceService WebSpaceService
<OnlyAdmin>
<AdminWebsitesNavigation Index="1"/>
<AdminWebspacesNavigation Index="1"/>
<div class="card">
<div class="card-header border-0 pt-5">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold fs-3 mb-1">
<TL>Plesk servers</TL>
<TL>Cloud panels</TL>
</span>
</h3>
<div class="card-toolbar">
<a href="/admin/websites/servers/new" class="btn btn-sm btn-light-success">
<a href="/admin/webspaces/servers/new" class="btn btn-sm btn-light-success">
<i class="bx bx-user-plus"></i>
<TL>New plesk server</TL>
<TL>New cloud panel</TL>
</a>
</div>
</div>
<div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="table-responsive">
<Table TableItem="PleskServer" Items="PleskServers" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Api url"))" Field="@(x => x.ApiUrl)" Sortable="true" Filterable="true"/>
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Table TableItem="CloudPanel" Items="CloudPanels" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Host)" Sortable="true" Filterable="true"/>
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Api url"))" Field="@(x => x.ApiUrl)" Sortable="true" Filterable="true"/>
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
@if (OnlineCache.ContainsKey(context))
@if (OnlineCache.TryGetValue(context, out var value))
{
if (OnlineCache[context])
if (value)
{
<span class="text-success">
<TL>Online</TL>
@ -59,14 +60,14 @@
}
</Template>
</Column>
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Edit"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Edit"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a href="/admin/websites/servers/edit/@(context.Id)/">
<a href="/admin/webspaces/servers/edit/@(context.Id)/">
<TL>Manage</TL>
</a>
</Template>
</Column>
<Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Column TableItem="CloudPanel" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<DeleteButton Confirm="true" OnClick="() => OnClick(context)">
</DeleteButton>
@ -82,22 +83,22 @@
@code
{
private PleskServer[] PleskServers;
private CloudPanel[] CloudPanels;
private LazyLoader LazyLoader;
private Dictionary<PleskServer, bool> OnlineCache = new();
private Dictionary<CloudPanel, bool> OnlineCache = new();
private Task Load(LazyLoader arg)
{
PleskServers = PleskServerRepository
CloudPanels = CloudPanelRepository
.Get()
.ToArray();
Task.Run(async () =>
{
foreach (var pleskServer in PleskServers)
foreach (var cloudPanel in CloudPanels)
{
OnlineCache.Add(pleskServer, await WebsiteService.IsHostUp(pleskServer));
OnlineCache.Add(cloudPanel, await WebSpaceService.IsHostUp(cloudPanel));
await InvokeAsync(StateHasChanged);
}
@ -106,9 +107,9 @@
return Task.CompletedTask;
}
private async Task OnClick(PleskServer pleskServer)
private async Task OnClick(CloudPanel pleskServer)
{
PleskServerRepository.Delete(pleskServer);
CloudPanelRepository.Delete(pleskServer);
await LazyLoader.Reload();
}

View file

@ -1,8 +1,10 @@
@page "/admin/websites/servers/new"
@page "/admin/webspaces/servers/new"
@using Moonlight.App.Repositories
@using Moonlight.App.Models.Forms
@using Moonlight.App.Database.Entities
@using Mappy.Net
@inject PleskServerRepository PleskServerRepository
@inject Repository<CloudPanel> CloudPanelRepository
@inject NavigationManager NavigationManager
<OnlyAdmin>
@ -14,6 +16,12 @@
<div class="input-group mb-5">
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Host</TL>
</label>
<div class="input-group mb-5">
<InputText @bind-Value="Model.Host" class="form-control"></InputText>
</div>
<label class="form-label">
<TL>Api Url</TL>
</label>
@ -37,18 +45,13 @@
@code
{
private PleskServerDataModel Model = new();
private CloudPanelDataModel Model = new();
private Task OnValidSubmit()
{
PleskServerRepository.Add(new()
{
Name = Model.Name,
ApiUrl = Model.ApiUrl,
ApiKey = Model.ApiKey
});
NavigationManager.NavigateTo("/admin/websites/servers");
CloudPanelRepository.Add(Mapper.Map<CloudPanel>(Model));
NavigationManager.NavigateTo("/admin/webspaces/servers");
return Task.CompletedTask;
}

View file

@ -1,4 +1,4 @@
@page "/website/{Id:int}/{Route?}"
@page "/webspace/{Id:int}/{Route?}"
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@ -6,22 +6,22 @@
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Services.Interop
@inject WebsiteRepository WebsiteRepository
@inject WebsiteService WebsiteService
@inject Repository<WebSpace> WebSpaceRepository
@inject WebSpaceService WebSpaceService
@inject ToastService ToastService
<LazyLoader Load="Load">
@if (CurrentWebsite == null)
@if (CurrentWebspace == null)
{
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
<div class="card-body text-center">
<h1 class="card-title">
<TL>Website not found</TL>
<TL>Webspace not found</TL>
</h1>
<p class="card-text fs-4">
<TL>A website with that id cannot be found or you have no access for this server</TL>
<TL>A webspace with that id cannot be found or you have no access for this webspace</TL>
</p>
</div>
</div>
@ -31,7 +31,7 @@
{
if (HostOnline)
{
<CascadingValue Value="CurrentWebsite">
<CascadingValue Value="CurrentWebspace">
@{
var index = 0;
@ -51,21 +51,21 @@
break;
}
<WebsiteNavigation Index="index" Website="CurrentWebsite" />
<WebSpaceNavigation Index="index" WebSpace="CurrentWebspace" />
@switch (Route)
{
case "files":
<WebsiteFiles />
<WebSpaceFiles />
break;
case "ftp":
<WebsiteFtp />
<WebSpaceFtp />
break;
case "databases":
<WebsiteDatabases />
<WebSpaceDatabases />
break;
default:
<WebsiteDashboard />
<WebSpaceDashboard />
break;
}
}
@ -101,32 +101,28 @@
[CascadingParameter]
public User User { get; set; }
private Website? CurrentWebsite;
private WebSpace? CurrentWebspace;
private bool HostOnline = false;
private async Task Load(LazyLoader lazyLoader)
{
CurrentWebsite = WebsiteRepository
CurrentWebspace = WebSpaceRepository
.Get()
.Include(x => x.PleskServer)
.Include(x => x.CloudPanel)
.Include(x => x.Owner)
.FirstOrDefault(x => x.Id == Id);
if (CurrentWebsite != null)
if (CurrentWebspace != null)
{
if (CurrentWebsite.Owner.Id != User!.Id && !User.Admin)
CurrentWebsite = null;
if (CurrentWebspace.Owner.Id != User!.Id && !User.Admin)
CurrentWebspace = null;
}
if (CurrentWebsite != null)
if (CurrentWebspace != null)
{
await lazyLoader.SetText("Checking host system online status");
HostOnline = await WebsiteService.IsHostUp(CurrentWebsite);
if (HostOnline)
{
}
HostOnline = await WebSpaceService.IsHostUp(CurrentWebspace);
}
}
}

View file

@ -1,4 +1,4 @@
@page "/websites/create"
@page "/webspaces/create"
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Forms
@ -6,7 +6,7 @@
@using Microsoft.EntityFrameworkCore
@inject SubscriptionService SubscriptionService
@inject WebsiteService WebsiteService
@inject WebSpaceService WebSpaceService
@inject WebsiteRepository WebsiteRepository
@inject SmartDeployService SmartDeployService
@inject SmartTranslateService SmartTranslateService
@ -137,9 +137,9 @@
.Include(x => x.Owner)
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("websites")).Amount)
{
var website = await WebsiteService.Create(Model.BaseDomain, User, PleskServer);
//var website = await WebsiteService.Create(Model.BaseDomain, User, PleskServer);
NavigationManager.NavigateTo($"/website/{website.Id}");
//NavigationManager.NavigateTo($"/website/{website.Id}");
}
}
}

View file

@ -1,17 +1,17 @@
@page "/websites"
@page "/webspaces"
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories
@using Microsoft.EntityFrameworkCore
@inject WebsiteRepository WebsiteRepository
@inject Repository<WebSpace> WebSpaceRepository
<LazyLoader Load="Load">
@if (Websites.Any())
@if (WebSpaces.Any())
{
foreach (var website in Websites)
foreach (var webSpace in WebSpaces)
{
<div class="row px-5 mb-5">
<a class="card card-body" href="/website/@(website.Id)">
<a class="card card-body" href="/webspace/@(webSpace.Id)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
@ -20,10 +20,10 @@
</div>
<div class="d-flex justify-content-start flex-column">
<span class="text-gray-800 text-hover-primary mb-1 fs-5">
@(website.BaseDomain)
@(webSpace.Domain)
</span>
<span class="text-gray-400 fw-semibold d-block fs-6">
<span class="text-gray-700">@(website.PleskServer.Name)</span>
<span class="text-gray-700">@(webSpace.CloudPanel.Name)</span>
</span>
</div>
</div>
@ -38,10 +38,10 @@
<div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5">
<div class="d-flex flex-column pe-0 pe-sm-10">
<h4 class="fw-semibold">
<TL>You have no websites</TL>
<TL>You have no webspaces</TL>
</h4>
<span>
<TL>We were not able to find any websites associated with your account</TL>
<TL>We were not able to find any webspaces associated with your account</TL>
</span>
</div>
</div>
@ -53,14 +53,14 @@
[CascadingParameter]
public User User { get; set; }
private Website[] Websites;
private WebSpace[] WebSpaces;
private Task Load(LazyLoader lazyLoader)
{
Websites = WebsiteRepository
WebSpaces = WebSpaceRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.PleskServer)
.Include(x => x.CloudPanel)
.Where(x => x.Owner.Id == User.Id)
.ToArray();