Merge pull request #338 from Moonlight-Panel/AddServiceImplementationApi
Added service implementation api and some service utils
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using Moonlight.App.Database.Entities.Store;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
using Moonlight.App.Models.Abstractions;
|
using Moonlight.App.Models.Abstractions;
|
||||||
|
using Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
namespace Moonlight.App.Actions.Dummy;
|
namespace Moonlight.App.Actions.Dummy;
|
||||||
|
|
||||||
|
|||||||
11
Moonlight/App/Actions/Dummy/DummyConfig.cs
Normal file
11
Moonlight/App/Actions/Dummy/DummyConfig.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Actions.Dummy;
|
||||||
|
|
||||||
|
public class DummyConfig
|
||||||
|
{
|
||||||
|
[Description("Some description")]
|
||||||
|
public string String { get; set; } = "";
|
||||||
|
public bool Boolean { get; set; }
|
||||||
|
public int Integer { get; set; }
|
||||||
|
}
|
||||||
25
Moonlight/App/Actions/Dummy/DummyServiceDefinition.cs
Normal file
25
Moonlight/App/Actions/Dummy/DummyServiceDefinition.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Moonlight.App.Actions.Dummy.Layouts;
|
||||||
|
using Moonlight.App.Actions.Dummy.Pages;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Actions.Dummy;
|
||||||
|
|
||||||
|
public class DummyServiceDefinition : ServiceDefinition
|
||||||
|
{
|
||||||
|
public override ServiceActions Actions => new DummyActions();
|
||||||
|
public override Type ConfigType => typeof(DummyConfig);
|
||||||
|
public override async Task BuildUserView(ServiceViewContext context)
|
||||||
|
{
|
||||||
|
context.Layout = ComponentHelper.FromType<DummyUser>();
|
||||||
|
|
||||||
|
await context.AddPage<DummyPage>("Demo", "/demo");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task BuildAdminView(ServiceViewContext context)
|
||||||
|
{
|
||||||
|
context.Layout = ComponentHelper.FromType<DummyAdmin>();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Moonlight/App/Actions/Dummy/Layouts/DummyAdmin.razor
Normal file
5
Moonlight/App/Actions/Dummy/Layouts/DummyAdmin.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<h3>DummyAdmin</h3>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
5
Moonlight/App/Actions/Dummy/Layouts/DummyUser.razor
Normal file
5
Moonlight/App/Actions/Dummy/Layouts/DummyUser.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<h3>DummyUser</h3>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
5
Moonlight/App/Actions/Dummy/Pages/DummyPage.razor
Normal file
5
Moonlight/App/Actions/Dummy/Pages/DummyPage.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<h3>DummyPage</h3>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,9 +4,20 @@ namespace Moonlight.App.Helpers;
|
|||||||
|
|
||||||
public static class ComponentHelper
|
public static class ComponentHelper
|
||||||
{
|
{
|
||||||
public static RenderFragment FromType(Type type) => builder =>
|
public static RenderFragment FromType(Type type, Action<Dictionary<string, object>>? buildAttributes = null) => builder =>
|
||||||
{
|
{
|
||||||
builder.OpenComponent(0, type);
|
builder.OpenComponent(0, type);
|
||||||
|
|
||||||
|
if (buildAttributes != null)
|
||||||
|
{
|
||||||
|
Dictionary<string, object> parameters = new();
|
||||||
|
buildAttributes.Invoke(parameters);
|
||||||
|
builder.AddMultipleAttributes(1, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static RenderFragment FromType<T>(Action<Dictionary<string, object>>? buildAttributes = null) where T : ComponentBase =>
|
||||||
|
FromType(typeof(T), buildAttributes);
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Moonlight.App.Database.Entities.Store;
|
|
||||||
|
|
||||||
namespace Moonlight.App.Models.Abstractions;
|
|
||||||
|
|
||||||
public abstract class ServiceActions
|
|
||||||
{
|
|
||||||
public abstract Task Create(IServiceProvider provider, Service service);
|
|
||||||
public abstract Task Update(IServiceProvider provider, Service service);
|
|
||||||
public abstract Task Delete(IServiceProvider provider, Service service);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
public abstract class ServiceActions
|
||||||
|
{
|
||||||
|
public abstract Task Create(IServiceProvider provider, Database.Entities.Store.Service service);
|
||||||
|
public abstract Task Update(IServiceProvider provider, Database.Entities.Store.Service service);
|
||||||
|
public abstract Task Delete(IServiceProvider provider, Database.Entities.Store.Service service);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
public abstract class ServiceDefinition
|
||||||
|
{
|
||||||
|
// Config
|
||||||
|
public abstract ServiceActions Actions { get; }
|
||||||
|
public abstract Type ConfigType { get; }
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
public abstract Task BuildUserView(ServiceViewContext context);
|
||||||
|
public abstract Task BuildAdminView(ServiceViewContext context);
|
||||||
|
}
|
||||||
11
Moonlight/App/Models/Abstractions/Services/ServiceUiPage.cs
Normal file
11
Moonlight/App/Models/Abstractions/Services/ServiceUiPage.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
public class ServiceUiPage
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Route { get; set; }
|
||||||
|
public string Icon { get; set; }
|
||||||
|
public RenderFragment Component { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
public class ServiceViewContext
|
||||||
|
{
|
||||||
|
// Meta
|
||||||
|
public Service Service { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public Product Product { get; set; }
|
||||||
|
|
||||||
|
// Content
|
||||||
|
public List<ServiceUiPage> Pages { get; set; } = new();
|
||||||
|
public RenderFragment Layout { get; set; }
|
||||||
|
|
||||||
|
public Task AddPage<T>(string name, string route, string icon = "") where T : ComponentBase
|
||||||
|
{
|
||||||
|
Pages.Add(new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Route = route,
|
||||||
|
Icon = icon,
|
||||||
|
Component = ComponentHelper.FromType<T>()
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ public enum Permission
|
|||||||
AdminUsersEdit = 1003,
|
AdminUsersEdit = 1003,
|
||||||
AdminTickets = 1004,
|
AdminTickets = 1004,
|
||||||
AdminCommunity = 1030,
|
AdminCommunity = 1030,
|
||||||
|
AdminServices = 1050,
|
||||||
AdminStore = 1900,
|
AdminStore = 1900,
|
||||||
AdminViewExceptions = 1999,
|
AdminViewExceptions = 1999,
|
||||||
AdminRoot = 2000
|
AdminRoot = 2000
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Moonlight.App.Plugins.Contexts;
|
using Moonlight.App.Models.Abstractions.Services;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Plugins.Contexts;
|
||||||
|
|
||||||
public class PluginContext
|
public class PluginContext
|
||||||
{
|
{
|
||||||
@@ -9,4 +11,6 @@ public class PluginContext
|
|||||||
public WebApplication WebApplication { get; set; }
|
public WebApplication WebApplication { get; set; }
|
||||||
public List<Action> PreInitTasks = new();
|
public List<Action> PreInitTasks = new();
|
||||||
public List<Action> PostInitTasks = new();
|
public List<Action> PostInitTasks = new();
|
||||||
|
public Action<ServiceViewContext>? BuildUserServiceView { get; set; } = null;
|
||||||
|
public Action<ServiceViewContext>? BuildAdminServiceView { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.Interop;
|
namespace Moonlight.App.Services.Interop;
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ public class CookieService
|
|||||||
if(string.IsNullOrEmpty(cookiePart))
|
if(string.IsNullOrEmpty(cookiePart))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var cookieKeyValue = cookiePart.Split("=");
|
var cookieKeyValue = cookiePart.Split("=")
|
||||||
|
.Select(x => x.Trim()) // There may be spaces e.g. with the "AspNetCore.Culture" key
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
if (cookieKeyValue.Length == 2)
|
if (cookieKeyValue.Length == 2)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Abstractions;
|
||||||
|
using Moonlight.App.Models.Abstractions.Services;
|
||||||
using Moonlight.App.Plugins;
|
using Moonlight.App.Plugins;
|
||||||
using Moonlight.App.Plugins.Contexts;
|
using Moonlight.App.Plugins.Contexts;
|
||||||
|
|
||||||
@@ -105,6 +109,26 @@ public class PluginService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task BuildUserServiceView(ServiceViewContext context)
|
||||||
|
{
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
plugin.Context.BuildUserServiceView?.Invoke(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task BuildAdminServiceView(ServiceViewContext context)
|
||||||
|
{
|
||||||
|
foreach (var plugin in Plugins)
|
||||||
|
{
|
||||||
|
plugin.Context.BuildAdminServiceView?.Invoke(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private string[] FindFiles(string dir)
|
private string[] FindFiles(string dir)
|
||||||
{
|
{
|
||||||
var result = new List<string>();
|
var result = new List<string>();
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Database.Entities.Store;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
using Moonlight.App.Database.Enums;
|
|
||||||
using Moonlight.App.Exceptions;
|
using Moonlight.App.Exceptions;
|
||||||
using Moonlight.App.Models.Abstractions;
|
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.ServiceManage;
|
namespace Moonlight.App.Services.ServiceManage;
|
||||||
|
|
||||||
public class ServiceAdminService
|
public class ServiceAdminService
|
||||||
{
|
{
|
||||||
public readonly Dictionary<ServiceType, ServiceActions> Actions = new();
|
|
||||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
private readonly ServiceDefinitionService ServiceDefinitionService;
|
||||||
|
|
||||||
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory)
|
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory, ServiceDefinitionService serviceDefinitionService)
|
||||||
{
|
{
|
||||||
ServiceScopeFactory = serviceScopeFactory;
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
ServiceDefinitionService = serviceDefinitionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
|
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
|
||||||
{
|
{
|
||||||
if (!Actions.ContainsKey(p.Type))
|
var impl = ServiceDefinitionService.Get(p);
|
||||||
throw new DisplayException($"The product type {p.Type} is not registered");
|
|
||||||
|
|
||||||
// Load models in new scope
|
// Load models in new scope
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
@@ -49,8 +47,7 @@ public class ServiceAdminService
|
|||||||
var finishedService = serviceRepo.Add(service);
|
var finishedService = serviceRepo.Add(service);
|
||||||
|
|
||||||
// Call the action for the logic behind the service type
|
// Call the action for the logic behind the service type
|
||||||
var actions = Actions[product.Type];
|
await impl.Actions.Create(scope.ServiceProvider, finishedService);
|
||||||
await actions.Create(scope.ServiceProvider, finishedService);
|
|
||||||
|
|
||||||
return finishedService;
|
return finishedService;
|
||||||
}
|
}
|
||||||
@@ -63,17 +60,15 @@ public class ServiceAdminService
|
|||||||
|
|
||||||
var service = serviceRepo
|
var service = serviceRepo
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Product)
|
|
||||||
.Include(x => x.Shares)
|
.Include(x => x.Shares)
|
||||||
.FirstOrDefault(x => x.Id == s.Id);
|
.FirstOrDefault(x => x.Id == s.Id);
|
||||||
|
|
||||||
if (service == null)
|
if (service == null)
|
||||||
throw new DisplayException("Service does not exist anymore");
|
throw new DisplayException("Service does not exist anymore");
|
||||||
|
|
||||||
if (!Actions.ContainsKey(service.Product.Type))
|
var impl = ServiceDefinitionService.Get(service);
|
||||||
throw new DisplayException($"The product type {service.Product.Type} is not registered");
|
|
||||||
|
|
||||||
await Actions[service.Product.Type].Delete(scope.ServiceProvider, service);
|
await impl.Actions.Delete(scope.ServiceProvider, service);
|
||||||
|
|
||||||
foreach (var share in service.Shares.ToArray())
|
foreach (var share in service.Shares.ToArray())
|
||||||
{
|
{
|
||||||
@@ -82,10 +77,4 @@ public class ServiceAdminService
|
|||||||
|
|
||||||
serviceRepo.Delete(service);
|
serviceRepo.Delete(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task RegisterAction(ServiceType type, ServiceActions actions) // Use this function to register service types
|
|
||||||
{
|
|
||||||
Actions.Add(type, actions);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
using Moonlight.App.Models.Abstractions;
|
||||||
|
using Moonlight.App.Models.Abstractions.Services;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.ServiceManage;
|
||||||
|
|
||||||
|
public class ServiceDefinitionService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<ServiceType, ServiceDefinition> ServiceImplementations = new();
|
||||||
|
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
|
public ServiceDefinitionService(IServiceScopeFactory serviceScopeFactory)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register<T>(ServiceType type) where T : ServiceDefinition
|
||||||
|
{
|
||||||
|
var impl = Activator.CreateInstance<T>() as ServiceDefinition;
|
||||||
|
|
||||||
|
if (impl == null)
|
||||||
|
throw new ArgumentException("The provided type is not an service implementation");
|
||||||
|
|
||||||
|
if (ServiceImplementations.ContainsKey(type))
|
||||||
|
throw new ArgumentException($"An implementation for {type} has already been registered");
|
||||||
|
|
||||||
|
ServiceImplementations.Add(type, impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceDefinition Get(Service s)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||||
|
|
||||||
|
var service = serviceRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Product)
|
||||||
|
.First(x => x.Id == s.Id);
|
||||||
|
|
||||||
|
return Get(service.Product);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceDefinition Get(Product p) => Get(p.Type);
|
||||||
|
|
||||||
|
public ServiceDefinition Get(ServiceType type)
|
||||||
|
{
|
||||||
|
if (!ServiceImplementations.ContainsKey(type))
|
||||||
|
throw new ArgumentException($"No service implementation found for {type}");
|
||||||
|
|
||||||
|
return ServiceImplementations[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Moonlight/App/Services/ServiceManage/ServiceManageService.cs
Normal file
61
Moonlight/App/Services/ServiceManage/ServiceManageService.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Models.Abstractions;
|
||||||
|
using Moonlight.App.Models.Enums;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.ServiceManage;
|
||||||
|
|
||||||
|
public class ServiceManageService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
|
||||||
|
public ServiceManageService(IServiceScopeFactory serviceScopeFactory)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> CheckAccess(Service s, User user)
|
||||||
|
{
|
||||||
|
var permissionStorage = new PermissionStorage(user.Permissions);
|
||||||
|
|
||||||
|
// Is admin?
|
||||||
|
if(permissionStorage[Permission.AdminServices])
|
||||||
|
return Task.FromResult(true);
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||||
|
|
||||||
|
var service = serviceRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Include(x => x.Shares)
|
||||||
|
.ThenInclude(x => x.User)
|
||||||
|
.First(x => x.Id == s.Id);
|
||||||
|
|
||||||
|
// Is owner?
|
||||||
|
if(service.Owner.Id == user.Id)
|
||||||
|
return Task.FromResult(true);
|
||||||
|
|
||||||
|
// Is shared user
|
||||||
|
if(service.Shares.Any(x => x.User.Id == user.Id))
|
||||||
|
return Task.FromResult(true);
|
||||||
|
|
||||||
|
// No match
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> NeedsRenewal(Service s)
|
||||||
|
{
|
||||||
|
// We fetch the service in a new scope wo ensure that we are not caching
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||||
|
|
||||||
|
var service = serviceRepo
|
||||||
|
.Get()
|
||||||
|
.First(x => x.Id == s.Id);
|
||||||
|
|
||||||
|
return Task.FromResult(DateTime.UtcNow > service.RenewAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ public class ServiceService // This service is used for managing services and cr
|
|||||||
private readonly Repository<User> UserRepository;
|
private readonly Repository<User> UserRepository;
|
||||||
|
|
||||||
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
|
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
|
||||||
|
public ServiceDefinitionService Definition => ServiceProvider.GetRequiredService<ServiceDefinitionService>();
|
||||||
|
public ServiceManageService Manage => ServiceProvider.GetRequiredService<ServiceManageService>();
|
||||||
|
|
||||||
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository, Repository<User> userRepository)
|
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository, Repository<User> userRepository)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
using Moonlight.App.Database.Enums;
|
using Moonlight.App.Database.Enums;
|
||||||
using Moonlight.App.Exceptions;
|
using Moonlight.App.Exceptions;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
|
using Moonlight.App.Services.ServiceManage;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.Store;
|
namespace Moonlight.App.Services.Store;
|
||||||
|
|
||||||
@@ -9,11 +11,16 @@ public class StoreAdminService
|
|||||||
{
|
{
|
||||||
private readonly Repository<Product> ProductRepository;
|
private readonly Repository<Product> ProductRepository;
|
||||||
private readonly Repository<Category> CategoryRepository;
|
private readonly Repository<Category> CategoryRepository;
|
||||||
|
private readonly ServiceService ServiceService;
|
||||||
|
|
||||||
public StoreAdminService(Repository<Product> productRepository, Repository<Category> categoryRepository)
|
public StoreAdminService(
|
||||||
|
Repository<Product> productRepository,
|
||||||
|
Repository<Category> categoryRepository,
|
||||||
|
ServiceService serviceService)
|
||||||
{
|
{
|
||||||
ProductRepository = productRepository;
|
ProductRepository = productRepository;
|
||||||
CategoryRepository = categoryRepository;
|
CategoryRepository = categoryRepository;
|
||||||
|
ServiceService = serviceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Category> AddCategory(string name, string description, string slug)
|
public Task<Category> AddCategory(string name, string description, string slug)
|
||||||
@@ -31,8 +38,7 @@ public class StoreAdminService
|
|||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, string configJson,
|
public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, Action<Product>? modifyProduct = null)
|
||||||
Action<Product>? modifyProduct = null)
|
|
||||||
{
|
{
|
||||||
if (ProductRepository.Get().Any(x => x.Slug == slug))
|
if (ProductRepository.Get().Any(x => x.Slug == slug))
|
||||||
throw new DisplayException("A product with that slug does already exist");
|
throw new DisplayException("A product with that slug does already exist");
|
||||||
@@ -43,7 +49,7 @@ public class StoreAdminService
|
|||||||
Description = description,
|
Description = description,
|
||||||
Slug = slug,
|
Slug = slug,
|
||||||
Type = type,
|
Type = type,
|
||||||
ConfigJson = configJson
|
ConfigJson = "{}"
|
||||||
};
|
};
|
||||||
|
|
||||||
if(modifyProduct != null)
|
if(modifyProduct != null)
|
||||||
@@ -96,4 +102,36 @@ public class StoreAdminService
|
|||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Product config
|
||||||
|
public Type GetProductConfigType(ServiceType type)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var impl = ServiceService.Definition.Get(type);
|
||||||
|
return impl.ConfigType;
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return typeof(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public object CreateNewProductConfig(ServiceType type)
|
||||||
|
{
|
||||||
|
var config = Activator.CreateInstance(GetProductConfigType(type))!;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
public object GetProductConfig(Product product)
|
||||||
|
{
|
||||||
|
var impl = ServiceService.Definition.Get(product.Type);
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject(product.ConfigJson, impl.ConfigType) ??
|
||||||
|
CreateNewProductConfig(product.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveProductConfig(Product product, object config)
|
||||||
|
{
|
||||||
|
product.ConfigJson = JsonConvert.SerializeObject(config);
|
||||||
|
ProductRepository.Update(product);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace Moonlight.App.Services.Users;
|
|||||||
public class UserDeleteService
|
public class UserDeleteService
|
||||||
{
|
{
|
||||||
private readonly Repository<Service> ServiceRepository;
|
private readonly Repository<Service> ServiceRepository;
|
||||||
|
private readonly Repository<ServiceShare> ServiceShareRepository;
|
||||||
private readonly Repository<Post> PostRepository;
|
private readonly Repository<Post> PostRepository;
|
||||||
private readonly Repository<User> UserRepository;
|
private readonly Repository<User> UserRepository;
|
||||||
private readonly Repository<Transaction> TransactionRepository;
|
private readonly Repository<Transaction> TransactionRepository;
|
||||||
@@ -32,7 +33,8 @@ public class UserDeleteService
|
|||||||
Repository<CouponUse> couponUseRepository,
|
Repository<CouponUse> couponUseRepository,
|
||||||
Repository<Transaction> transactionRepository,
|
Repository<Transaction> transactionRepository,
|
||||||
Repository<Ticket> ticketRepository,
|
Repository<Ticket> ticketRepository,
|
||||||
Repository<TicketMessage> ticketMessageRepository)
|
Repository<TicketMessage> ticketMessageRepository,
|
||||||
|
Repository<ServiceShare> serviceShareRepository)
|
||||||
{
|
{
|
||||||
ServiceRepository = serviceRepository;
|
ServiceRepository = serviceRepository;
|
||||||
ServiceService = serviceService;
|
ServiceService = serviceService;
|
||||||
@@ -44,6 +46,7 @@ public class UserDeleteService
|
|||||||
TransactionRepository = transactionRepository;
|
TransactionRepository = transactionRepository;
|
||||||
TicketRepository = ticketRepository;
|
TicketRepository = ticketRepository;
|
||||||
TicketMessageRepository = ticketMessageRepository;
|
TicketMessageRepository = ticketMessageRepository;
|
||||||
|
ServiceShareRepository = serviceShareRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Perform(User user)
|
public async Task Perform(User user)
|
||||||
@@ -83,6 +86,17 @@ public class UserDeleteService
|
|||||||
await ServiceService.Admin.Delete(service);
|
await ServiceService.Admin.Delete(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service shares
|
||||||
|
var shares = ServiceShareRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.User.Id == user.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var share in shares)
|
||||||
|
{
|
||||||
|
ServiceShareRepository.Delete(share);
|
||||||
|
}
|
||||||
|
|
||||||
// Transactions - Coupons - Gift codes
|
// Transactions - Coupons - Gift codes
|
||||||
var userWithDetails = UserRepository
|
var userWithDetails = UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using JWT.Algorithms;
|
using JWT.Algorithms;
|
||||||
using JWT.Builder;
|
using JWT.Builder;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.Utils;
|
namespace Moonlight.App.Services.Utils;
|
||||||
@@ -47,6 +48,7 @@ public class JwtService
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Logger.Warn(e.Message);
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ builder.Services.AddSingleton<AutoMailSendService>();
|
|||||||
// Services / ServiceManage
|
// Services / ServiceManage
|
||||||
builder.Services.AddScoped<ServiceService>();
|
builder.Services.AddScoped<ServiceService>();
|
||||||
builder.Services.AddSingleton<ServiceAdminService>();
|
builder.Services.AddSingleton<ServiceAdminService>();
|
||||||
|
builder.Services.AddSingleton<ServiceDefinitionService>();
|
||||||
|
builder.Services.AddSingleton<ServiceManageService>();
|
||||||
|
|
||||||
// Services / Ticketing
|
// Services / Ticketing
|
||||||
builder.Services.AddScoped<TicketService>();
|
builder.Services.AddScoped<TicketService>();
|
||||||
@@ -121,8 +123,9 @@ app.MapControllers();
|
|||||||
// Auto start background services
|
// Auto start background services
|
||||||
app.Services.GetRequiredService<AutoMailSendService>();
|
app.Services.GetRequiredService<AutoMailSendService>();
|
||||||
|
|
||||||
var serviceService = app.Services.GetRequiredService<ServiceAdminService>();
|
var serviceService = app.Services.GetRequiredService<ServiceDefinitionService>();
|
||||||
await serviceService.RegisterAction(ServiceType.Server, new DummyActions());
|
|
||||||
|
serviceService.Register<DummyServiceDefinition>(ServiceType.Server);
|
||||||
|
|
||||||
await pluginService.RunPrePost(app);
|
await pluginService.RunPrePost(app);
|
||||||
|
|
||||||
|
|||||||
16
Moonlight/Shared/Components/Alerts/NeedsRenewalAlert.razor
Normal file
16
Moonlight/Shared/Components/Alerts/NeedsRenewalAlert.razor
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="d-flex flex-column flex-center text-center p-10">
|
||||||
|
<div class="card card-flush w-lg-650px py-5">
|
||||||
|
<div class="card-body py-15 py-lg-20">
|
||||||
|
<div class="mb-5">
|
||||||
|
<img src="/svg/expired.svg" style="width: 10vh" alt="Expired illustration">
|
||||||
|
</div>
|
||||||
|
<h1 class="fw-bolder fs-2hx text-gray-900 mb-4">
|
||||||
|
This service has expired
|
||||||
|
</h1>
|
||||||
|
<div class="fw-semibold fs-6 text-gray-500 mb-7">
|
||||||
|
<span class="fs-5">This service has expired and has to be renewed in order to manage it</span>
|
||||||
|
</div>
|
||||||
|
<a href="/services" class="btn btn-primary">Go back to services</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -5,15 +5,16 @@
|
|||||||
@foreach (var prop in typeof(TForm).GetProperties())
|
@foreach (var prop in typeof(TForm).GetProperties())
|
||||||
{
|
{
|
||||||
<div class="col-md-@(Columns) col-12">
|
<div class="col-md-@(Columns) col-12">
|
||||||
<CascadingValue Name="Property" Value="prop">
|
|
||||||
<CascadingValue Name="Data" Value="(object)Model">
|
|
||||||
@{
|
@{
|
||||||
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
||||||
|
var rf = ComponentHelper.FromType(typeToCreate, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("Data", Model);
|
||||||
|
parameters.Add("Property", prop);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ComponentHelper.FromType(typeToCreate)
|
@rf
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,10 +119,10 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[CascadingParameter(Name = "Data")]
|
[Parameter]
|
||||||
public object Data { get; set; }
|
public object Data { get; set; }
|
||||||
|
|
||||||
[CascadingParameter(Name = "Property")]
|
[Parameter]
|
||||||
public PropertyInfo Property { get; set; }
|
public PropertyInfo Property { get; set; }
|
||||||
|
|
||||||
private PropBinder<TProp> Binder;
|
private PropBinder<TProp> Binder;
|
||||||
|
|||||||
19
Moonlight/Shared/Components/Forms/DynamicTypedAutoForm.razor
Normal file
19
Moonlight/Shared/Components/Forms/DynamicTypedAutoForm.razor
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@{
|
||||||
|
var typeToCreate = typeof(AutoForm<>).MakeGenericType(Model.GetType());
|
||||||
|
var rf = ComponentHelper.FromType(typeToCreate, parameter =>
|
||||||
|
{
|
||||||
|
parameter.Add("Model", Model);
|
||||||
|
parameter.Add("Columns", Columns);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@rf
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public object Model { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int Columns { get; set; } = 6;
|
||||||
|
}
|
||||||
@@ -16,6 +16,11 @@
|
|||||||
<i class="bx bx-sm bx-gift me-2"></i> Gifts
|
<i class="bx bx-sm bx-gift me-2"></i> Gifts
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/admin/store/expired">
|
||||||
|
<i class="bx bx-sm bx-timer me-2"></i> Expired services
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,6 +84,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link " href="/admin/services">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-sm bx-cube"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
Services
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<a class="menu-link " href="/admin/store">
|
<a class="menu-link " href="/admin/store">
|
||||||
<span class="menu-icon">
|
<span class="menu-icon">
|
||||||
|
|||||||
@@ -128,7 +128,13 @@ else
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title align-items-start flex-column">
|
<h3 class="card-title align-items-start flex-column">
|
||||||
<span class="card-label fw-bold text-dark fs-3">@(Service.Nickname ?? $"Service {Service.Id}")</span>
|
<span class="card-label fw-bold text-dark fs-3">
|
||||||
|
@(Service.Nickname ?? $"Service {Service.Id}")
|
||||||
|
@if (NeedsRenewal)
|
||||||
|
{
|
||||||
|
<span class="ms-2 text-danger">(Expired)</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(Service.Product.Name)</span>
|
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(Service.Product.Name)</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,14 +183,28 @@ else
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<Task> OnChange { get; set; }
|
public Func<Task> OnChange { get; set; }
|
||||||
|
|
||||||
|
// Renew access state
|
||||||
|
private bool NeedsRenewal = false;
|
||||||
|
|
||||||
|
// States
|
||||||
private bool ShowDeletionScreen = false;
|
private bool ShowDeletionScreen = false;
|
||||||
private bool ShowRenewScreen = false;
|
|
||||||
private ManageServiceShareModal ShareModal;
|
private ManageServiceShareModal ShareModal;
|
||||||
|
|
||||||
|
// Renewing
|
||||||
private int DurationMultiplier = 1;
|
private int DurationMultiplier = 1;
|
||||||
private bool CanBeRenewed = false;
|
private bool CanBeRenewed = false;
|
||||||
private bool IsValidating = false;
|
private bool IsValidating = false;
|
||||||
private string ErrorMessage = "";
|
private string ErrorMessage = "";
|
||||||
|
private bool ShowRenewScreen = false;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
NeedsRenewal = await ServiceService.Manage.NeedsRenewal(Service);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Task Revalidate()
|
private Task Revalidate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,14 +109,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Type</label>
|
<label class="form-label">Type</label>
|
||||||
<SmartEnumSelect @bind-Value="AddProductForm.Type"/>
|
<SmartEnumSelect @bind-Value="AddProductServiceType"/>
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Config</label>
|
|
||||||
<input @bind="AddProductForm.ConfigJson" class="form-control" type="text"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<DynamicTypedAutoForm Model="AddProductConfig" Columns="6"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
@@ -172,14 +171,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Type</label>
|
<label class="form-label">Type</label>
|
||||||
<SmartEnumSelect @bind-Value="EditProductForm.Type"/>
|
<SmartEnumSelect @bind-Value="EditProductServiceType"/>
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Config</label>
|
|
||||||
<input @bind="EditProductForm.ConfigJson" class="form-control" type="text"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<DynamicTypedAutoForm Model="EditProductConfig" Columns="6"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
@@ -231,6 +229,7 @@
|
|||||||
EditCategoryForm = Mapper.Map<EditCategoryForm>(EditCategory);
|
EditCategoryForm = Mapper.Map<EditCategoryForm>(EditCategory);
|
||||||
await EditCategoryModal.Show();
|
await EditCategoryModal.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EditCategorySubmit()
|
private async Task EditCategorySubmit()
|
||||||
{
|
{
|
||||||
EditCategory = Mapper.Map(EditCategory, EditCategoryForm);
|
EditCategory = Mapper.Map(EditCategory, EditCategoryForm);
|
||||||
@@ -250,17 +249,30 @@
|
|||||||
private SmartModal AddProductModal;
|
private SmartModal AddProductModal;
|
||||||
private AddProductForm AddProductForm = new();
|
private AddProductForm AddProductForm = new();
|
||||||
private Category[] Categories;
|
private Category[] Categories;
|
||||||
|
private object AddProductConfig = new();
|
||||||
|
|
||||||
|
private ServiceType AddProductServiceType
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (AddProductConfig.GetType() != StoreService.Admin.GetProductConfigType(value))
|
||||||
|
AddProductConfig = StoreService.Admin.CreateNewProductConfig(value);
|
||||||
|
|
||||||
|
AddProductForm.Type = value;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
get => AddProductForm.Type;
|
||||||
|
}
|
||||||
|
|
||||||
public Task AddProductShow => AddProductModal.Show();
|
public Task AddProductShow => AddProductModal.Show();
|
||||||
|
|
||||||
private async Task AddProductSubmit()
|
private async Task AddProductSubmit()
|
||||||
{
|
{
|
||||||
await StoreService.Admin.AddProduct(
|
var product = await StoreService.Admin.AddProduct(
|
||||||
AddProductForm.Name,
|
AddProductForm.Name,
|
||||||
AddProductForm.Description,
|
AddProductForm.Description,
|
||||||
AddProductForm.Slug,
|
AddProductForm.Slug,
|
||||||
AddProductForm.Type,
|
AddProductForm.Type,
|
||||||
AddProductForm.ConfigJson,
|
|
||||||
product =>
|
product =>
|
||||||
{
|
{
|
||||||
product.Category = AddProductForm.Category;
|
product.Category = AddProductForm.Category;
|
||||||
@@ -271,6 +283,8 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
StoreService.Admin.SaveProductConfig(product, AddProductConfig);
|
||||||
|
|
||||||
await ToastService.Success("Successfully added product");
|
await ToastService.Success("Successfully added product");
|
||||||
await AddProductModal.Hide();
|
await AddProductModal.Hide();
|
||||||
|
|
||||||
@@ -285,10 +299,25 @@
|
|||||||
private SmartModal EditProductModal;
|
private SmartModal EditProductModal;
|
||||||
private EditProductForm EditProductForm = new();
|
private EditProductForm EditProductForm = new();
|
||||||
private Product EditProduct;
|
private Product EditProduct;
|
||||||
|
private object EditProductConfig = new();
|
||||||
|
|
||||||
|
private ServiceType EditProductServiceType
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (EditProductConfig.GetType() != StoreService.Admin.GetProductConfigType(value))
|
||||||
|
EditProductConfig = StoreService.Admin.CreateNewProductConfig(value);
|
||||||
|
|
||||||
|
EditProductForm.Type = value;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
get => EditProductForm.Type;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task EditProductShow(Product product)
|
public async Task EditProductShow(Product product)
|
||||||
{
|
{
|
||||||
EditProduct = product;
|
EditProduct = product;
|
||||||
|
EditProductConfig = StoreService.Admin.GetProductConfig(product);
|
||||||
|
|
||||||
EditProductForm = Mapper.Map<EditProductForm>(EditProduct);
|
EditProductForm = Mapper.Map<EditProductForm>(EditProduct);
|
||||||
await EditProductModal.Show();
|
await EditProductModal.Show();
|
||||||
@@ -299,6 +328,7 @@
|
|||||||
EditProduct = Mapper.Map(EditProduct, EditProductForm);
|
EditProduct = Mapper.Map(EditProduct, EditProductForm);
|
||||||
|
|
||||||
await StoreService.Admin.UpdateProduct(EditProduct);
|
await StoreService.Admin.UpdateProduct(EditProduct);
|
||||||
|
StoreService.Admin.SaveProductConfig(EditProduct, EditProductConfig);
|
||||||
|
|
||||||
await ToastService.Success("Successfully updated product");
|
await ToastService.Success("Successfully updated product");
|
||||||
await EditProductModal.Hide();
|
await EditProductModal.Hide();
|
||||||
|
|||||||
71
Moonlight/Shared/Views/Admin/Services/Index.razor
Normal file
71
Moonlight/Shared/Views/Admin/Services/Index.razor
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
@page "/admin/services"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using BlazorTable
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminServices)]
|
||||||
|
|
||||||
|
@inject Repository<Service> ServiceRepository
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Services</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<Table TableItem="Service"
|
||||||
|
Items="Services"
|
||||||
|
PageSize="50"
|
||||||
|
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||||
|
TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Id)" Title="Id" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Nickname)" Title="Name" Filterable="true" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/services/view/@(context.Id)">@(context.Nickname ?? $"Service {context.Id}")</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Owner)" Title="Owner" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Owner.Username)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Product)" Title="Type" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Product.Type)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Product)" Title="Product" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Product.Name)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.CreatedAt)" Title="Created at" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Id)" Title="" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/service/@(context.Id)">View as user</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
</Table>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private Service[] Services;
|
||||||
|
|
||||||
|
private Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
Services = ServiceRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Include(x => x.Product)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Moonlight/Shared/Views/Admin/Services/View.razor
Normal file
81
Moonlight/Shared/Views/Admin/Services/View.razor
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
@page "/admin/services/view/{Id:int}/{Route?}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Services.ServiceManage
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Abstractions.Services
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminServices)]
|
||||||
|
|
||||||
|
@inject Repository<Service> ServiceRepository
|
||||||
|
@inject ServiceService ServiceService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@inject PluginService PluginService
|
||||||
|
|
||||||
|
<LazyLoader Load="Load" ShowAsCard="true">
|
||||||
|
@if (Service == null)
|
||||||
|
{
|
||||||
|
<NotFoundAlert />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<CascadingValue Name="Service" Value="Service">
|
||||||
|
<CascadingValue Name="Implementation" Value="Definition">
|
||||||
|
<CascadingValue Name="Route" Value="Route">
|
||||||
|
<CascadingValue Name="ViewContext" Value="ViewContext">
|
||||||
|
@ViewContext.Layout
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Route { get; set; }
|
||||||
|
|
||||||
|
private Service? Service;
|
||||||
|
private ServiceDefinition Definition;
|
||||||
|
private ServiceViewContext ViewContext;
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Requesting service");
|
||||||
|
|
||||||
|
// Load service with relational data
|
||||||
|
Service = ServiceRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Product)
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.FirstOrDefault(x => x.Id == Id);
|
||||||
|
|
||||||
|
if(Service == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Load implementation
|
||||||
|
await lazyLoader.SetText("Loading implementation");
|
||||||
|
Definition = ServiceService.Definition.Get(Service.Product.Type);
|
||||||
|
|
||||||
|
// Build dynamic user interface
|
||||||
|
await lazyLoader.SetText("Building dynamic user interface");
|
||||||
|
|
||||||
|
ViewContext = new ServiceViewContext()
|
||||||
|
{
|
||||||
|
Service = Service,
|
||||||
|
Product = Service.Product,
|
||||||
|
User = IdentityService.CurrentUser
|
||||||
|
};
|
||||||
|
|
||||||
|
await Definition.BuildAdminView(ViewContext);
|
||||||
|
await PluginService.BuildAdminServiceView(ViewContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Moonlight/Shared/Views/Admin/Store/Expired.razor
Normal file
74
Moonlight/Shared/Views/Admin/Store/Expired.razor
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
@page "/admin/store/expired"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using BlazorTable
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminStore)]
|
||||||
|
|
||||||
|
@inject Repository<Service> ServiceRepository
|
||||||
|
|
||||||
|
<AdminStoreNavigation Index="3"/>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Expired services</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<Table TableItem="Service"
|
||||||
|
Items="Services"
|
||||||
|
PageSize="50"
|
||||||
|
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||||
|
TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Id)" Title="Id" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Nickname)" Title="Name" Filterable="true" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/services/view/@(context.Id)">@(context.Nickname ?? $"Service {context.Id}")</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Owner)" Title="Owner" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Owner.Username)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Product)" Title="Type" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Product.Type)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.Product)" Title="Product" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<span>@(context.Product.Name)</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.CreatedAt)" Title="Created at" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Service" Field="@(x => x.RenewAt)" Title="" Filterable="false" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>Expired since @(Formatter.FormatAgoFromDateTime(context.RenewAt))</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
</Table>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private Service[] Services;
|
||||||
|
|
||||||
|
private Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
Services = ServiceRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Include(x => x.Product)
|
||||||
|
.Where(x => x.RenewAt < DateTime.UtcNow)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,15 +83,16 @@ else
|
|||||||
@foreach (var prop in Properties)
|
@foreach (var prop in Properties)
|
||||||
{
|
{
|
||||||
<div class="col-md-6 col-12">
|
<div class="col-md-6 col-12">
|
||||||
<CascadingValue Name="Property" Value="prop">
|
|
||||||
<CascadingValue Name="Data" Value="ModelToShow">
|
|
||||||
@{
|
@{
|
||||||
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
||||||
|
var rf = ComponentHelper.FromType(typeToCreate, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("Data", ModelToShow);
|
||||||
|
parameters.Add("Property", prop);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ComponentHelper.FromType(typeToCreate)
|
@rf
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
|
|||||||
98
Moonlight/Shared/Views/Service/Index.razor
Normal file
98
Moonlight/Shared/Views/Service/Index.razor
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
@page "/service/{Id:int}/{Route?}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Services.ServiceManage
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Models.Abstractions.Services
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
|
||||||
|
@inject Repository<Service> ServiceRepository
|
||||||
|
@inject ServiceService ServiceService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@inject PluginService PluginService
|
||||||
|
|
||||||
|
<LazyLoader Load="Load" ShowAsCard="true">
|
||||||
|
@if (Service == null)
|
||||||
|
{
|
||||||
|
<NotFoundAlert />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (NeedsRenewal)
|
||||||
|
{
|
||||||
|
<NeedsRenewalAlert />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<CascadingValue Name="Service" Value="Service">
|
||||||
|
<CascadingValue Name="Implementation" Value="Definition">
|
||||||
|
<CascadingValue Name="Route" Value="Route">
|
||||||
|
<CascadingValue Name="ViewContext" Value="ViewContext">
|
||||||
|
@ViewContext.Layout
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Route { get; set; }
|
||||||
|
|
||||||
|
private Service? Service;
|
||||||
|
private ServiceDefinition Definition;
|
||||||
|
private ServiceViewContext ViewContext;
|
||||||
|
|
||||||
|
private bool NeedsRenewal = false;
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Requesting service");
|
||||||
|
|
||||||
|
// Load service with relational data
|
||||||
|
Service = ServiceRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Product)
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.FirstOrDefault(x => x.Id == Id);
|
||||||
|
|
||||||
|
if(Service == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
if (!await ServiceService.Manage.CheckAccess(Service, IdentityService.CurrentUser))
|
||||||
|
Service = null;
|
||||||
|
|
||||||
|
if (Service == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NeedsRenewal = await ServiceService.Manage.NeedsRenewal(Service);
|
||||||
|
|
||||||
|
if(NeedsRenewal) // Stop loading more data
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Load implementation
|
||||||
|
await lazyLoader.SetText("Loading implementation");
|
||||||
|
Definition = ServiceService.Definition.Get(Service.Product.Type);
|
||||||
|
|
||||||
|
// Build dynamic user interface
|
||||||
|
await lazyLoader.SetText("Building dynamic user interface");
|
||||||
|
|
||||||
|
ViewContext = new ServiceViewContext()
|
||||||
|
{
|
||||||
|
Service = Service,
|
||||||
|
Product = Service.Product,
|
||||||
|
User = IdentityService.CurrentUser
|
||||||
|
};
|
||||||
|
|
||||||
|
await Definition.BuildUserView(ViewContext);
|
||||||
|
await PluginService.BuildUserServiceView(ViewContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,19 @@
|
|||||||
|
|
||||||
@foreach (var service in SharedServices)
|
@foreach (var service in SharedServices)
|
||||||
{
|
{
|
||||||
|
var needsRenewal = SharedRenewalStates[service];
|
||||||
|
|
||||||
<div class="col-md-3 col-12">
|
<div class="col-md-3 col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title align-items-start flex-column">
|
<h3 class="card-title align-items-start flex-column">
|
||||||
<span class="card-label fw-bold text-dark fs-3">@(service.Nickname ?? $"Service {service.Id}")</span>
|
<span class="card-label fw-bold text-dark fs-3">
|
||||||
|
@(service.Nickname ?? $"Service {service.Id}")
|
||||||
|
@if (needsRenewal)
|
||||||
|
{
|
||||||
|
<span class="ms-2 text-danger">(Expired)</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
|
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer p-3 text-center">
|
<div class="card-footer p-3 text-center">
|
||||||
<button class="btn btn-primary">Manage</button>
|
<a href="/service/@(service.Id)" class="btn btn-primary">Manage</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,10 +65,19 @@
|
|||||||
|
|
||||||
private Service[] MyServices;
|
private Service[] MyServices;
|
||||||
private Service[] SharedServices;
|
private Service[] SharedServices;
|
||||||
|
private Dictionary<Service, bool> SharedRenewalStates = new();
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
|
// Load all services
|
||||||
MyServices = await ServiceService.Get(IdentityService.CurrentUser);
|
MyServices = await ServiceService.Get(IdentityService.CurrentUser);
|
||||||
SharedServices = await ServiceService.GetShared(IdentityService.CurrentUser);
|
SharedServices = await ServiceService.GetShared(IdentityService.CurrentUser);
|
||||||
|
|
||||||
|
// Load all services renewal states
|
||||||
|
foreach (var service in SharedServices)
|
||||||
|
{
|
||||||
|
if(!SharedRenewalStates.ContainsKey(service))
|
||||||
|
SharedRenewalStates.Add(service, await ServiceService.Manage.NeedsRenewal(service));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
Moonlight/wwwroot/svg/expired.svg
vendored
Normal file
1
Moonlight/wwwroot/svg/expired.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
Reference in New Issue
Block a user