Merge pull request #338 from Moonlight-Panel/AddServiceImplementationApi
Added service implementation api and some service utils
This commit is contained in:
commit
7145890801
39 changed files with 846 additions and 86 deletions
|
@ -1,5 +1,6 @@
|
|||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
using Moonlight.App.Models.Abstractions.Services;
|
||||
|
||||
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 RenderFragment FromType(Type type) => builder =>
|
||||
public static RenderFragment FromType(Type type, Action<Dictionary<string, object>>? buildAttributes = null) => builder =>
|
||||
{
|
||||
builder.OpenComponent(0, type);
|
||||
|
||||
if (buildAttributes != null)
|
||||
{
|
||||
Dictionary<string, object> parameters = new();
|
||||
buildAttributes.Invoke(parameters);
|
||||
builder.AddMultipleAttributes(1, parameters);
|
||||
}
|
||||
|
||||
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,
|
||||
AdminTickets = 1004,
|
||||
AdminCommunity = 1030,
|
||||
AdminServices = 1050,
|
||||
AdminStore = 1900,
|
||||
AdminViewExceptions = 1999,
|
||||
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
|
||||
{
|
||||
|
@ -9,4 +11,6 @@ public class PluginContext
|
|||
public WebApplication WebApplication { get; set; }
|
||||
public List<Action> PreInitTasks = 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 Moonlight.App.Helpers;
|
||||
|
||||
namespace Moonlight.App.Services.Interop;
|
||||
|
||||
|
@ -30,7 +31,9 @@ public class CookieService
|
|||
if(string.IsNullOrEmpty(cookiePart))
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
using System.Reflection;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
using Moonlight.App.Models.Abstractions.Services;
|
||||
using Moonlight.App.Plugins;
|
||||
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)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.ServiceManage;
|
||||
|
||||
public class ServiceAdminService
|
||||
{
|
||||
public readonly Dictionary<ServiceType, ServiceActions> Actions = new();
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
private readonly ServiceDefinitionService ServiceDefinitionService;
|
||||
|
||||
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory)
|
||||
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory, ServiceDefinitionService serviceDefinitionService)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
ServiceDefinitionService = serviceDefinitionService;
|
||||
}
|
||||
|
||||
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
|
||||
{
|
||||
if (!Actions.ContainsKey(p.Type))
|
||||
throw new DisplayException($"The product type {p.Type} is not registered");
|
||||
var impl = ServiceDefinitionService.Get(p);
|
||||
|
||||
// Load models in new scope
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
@ -49,8 +47,7 @@ public class ServiceAdminService
|
|||
var finishedService = serviceRepo.Add(service);
|
||||
|
||||
// Call the action for the logic behind the service type
|
||||
var actions = Actions[product.Type];
|
||||
await actions.Create(scope.ServiceProvider, finishedService);
|
||||
await impl.Actions.Create(scope.ServiceProvider, finishedService);
|
||||
|
||||
return finishedService;
|
||||
}
|
||||
|
@ -63,17 +60,15 @@ public class ServiceAdminService
|
|||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Shares)
|
||||
.FirstOrDefault(x => x.Id == s.Id);
|
||||
|
||||
if (service == null)
|
||||
throw new DisplayException("Service does not exist anymore");
|
||||
|
||||
if (!Actions.ContainsKey(service.Product.Type))
|
||||
throw new DisplayException($"The product type {service.Product.Type} is not registered");
|
||||
var impl = ServiceDefinitionService.Get(service);
|
||||
|
||||
await Actions[service.Product.Type].Delete(scope.ServiceProvider, service);
|
||||
await impl.Actions.Delete(scope.ServiceProvider, service);
|
||||
|
||||
foreach (var share in service.Shares.ToArray())
|
||||
{
|
||||
|
@ -82,10 +77,4 @@ public class ServiceAdminService
|
|||
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.ServiceManage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
|
@ -9,11 +11,16 @@ public class StoreAdminService
|
|||
{
|
||||
private readonly Repository<Product> ProductRepository;
|
||||
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;
|
||||
CategoryRepository = categoryRepository;
|
||||
ServiceService = serviceService;
|
||||
}
|
||||
|
||||
public Task<Category> AddCategory(string name, string description, string slug)
|
||||
|
@ -31,8 +38,7 @@ public class StoreAdminService
|
|||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, string configJson,
|
||||
Action<Product>? modifyProduct = null)
|
||||
public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, Action<Product>? modifyProduct = null)
|
||||
{
|
||||
if (ProductRepository.Get().Any(x => x.Slug == slug))
|
||||
throw new DisplayException("A product with that slug does already exist");
|
||||
|
@ -43,7 +49,7 @@ public class StoreAdminService
|
|||
Description = description,
|
||||
Slug = slug,
|
||||
Type = type,
|
||||
ConfigJson = configJson
|
||||
ConfigJson = "{}"
|
||||
};
|
||||
|
||||
if(modifyProduct != null)
|
||||
|
@ -96,4 +102,36 @@ public class StoreAdminService
|
|||
|
||||
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
|
||||
{
|
||||
private readonly Repository<Service> ServiceRepository;
|
||||
private readonly Repository<ServiceShare> ServiceShareRepository;
|
||||
private readonly Repository<Post> PostRepository;
|
||||
private readonly Repository<User> UserRepository;
|
||||
private readonly Repository<Transaction> TransactionRepository;
|
||||
|
@ -32,7 +33,8 @@ public class UserDeleteService
|
|||
Repository<CouponUse> couponUseRepository,
|
||||
Repository<Transaction> transactionRepository,
|
||||
Repository<Ticket> ticketRepository,
|
||||
Repository<TicketMessage> ticketMessageRepository)
|
||||
Repository<TicketMessage> ticketMessageRepository,
|
||||
Repository<ServiceShare> serviceShareRepository)
|
||||
{
|
||||
ServiceRepository = serviceRepository;
|
||||
ServiceService = serviceService;
|
||||
|
@ -44,6 +46,7 @@ public class UserDeleteService
|
|||
TransactionRepository = transactionRepository;
|
||||
TicketRepository = ticketRepository;
|
||||
TicketMessageRepository = ticketMessageRepository;
|
||||
ServiceShareRepository = serviceShareRepository;
|
||||
}
|
||||
|
||||
public async Task Perform(User user)
|
||||
|
@ -83,6 +86,17 @@ public class UserDeleteService
|
|||
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
|
||||
var userWithDetails = UserRepository
|
||||
.Get()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using JWT.Algorithms;
|
||||
using JWT.Builder;
|
||||
using Moonlight.App.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Moonlight.App.Services.Utils;
|
||||
|
@ -47,6 +48,7 @@ public class JwtService
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn(e.Message);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ builder.Services.AddSingleton<AutoMailSendService>();
|
|||
// Services / ServiceManage
|
||||
builder.Services.AddScoped<ServiceService>();
|
||||
builder.Services.AddSingleton<ServiceAdminService>();
|
||||
builder.Services.AddSingleton<ServiceDefinitionService>();
|
||||
builder.Services.AddSingleton<ServiceManageService>();
|
||||
|
||||
// Services / Ticketing
|
||||
builder.Services.AddScoped<TicketService>();
|
||||
|
@ -121,8 +123,9 @@ app.MapControllers();
|
|||
// Auto start background services
|
||||
app.Services.GetRequiredService<AutoMailSendService>();
|
||||
|
||||
var serviceService = app.Services.GetRequiredService<ServiceAdminService>();
|
||||
await serviceService.RegisterAction(ServiceType.Server, new DummyActions());
|
||||
var serviceService = app.Services.GetRequiredService<ServiceDefinitionService>();
|
||||
|
||||
serviceService.Register<DummyServiceDefinition>(ServiceType.Server);
|
||||
|
||||
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())
|
||||
{
|
||||
<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)
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
@rf
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
return prop.GetValue(x) as string ?? "N/A";
|
||||
});
|
||||
|
||||
<SmartDropdown @bind-Value="Binder.Class" DisplayFunc="displayFunc" SearchProp="searchFunc" Items="Items" />
|
||||
<SmartDropdown @bind-Value="Binder.Class" DisplayFunc="displayFunc" SearchProp="searchFunc" Items="Items"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -111,7 +111,7 @@
|
|||
return prop.GetValue(x) as string ?? "N/A";
|
||||
});
|
||||
|
||||
<SmartSelect @bind-Value="Binder.Class" DisplayField="displayFunc" Items="Items" CanBeNull="true" />
|
||||
<SmartSelect @bind-Value="Binder.Class" DisplayField="displayFunc" Items="Items" CanBeNull="true"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,10 +119,10 @@
|
|||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter(Name = "Data")]
|
||||
[Parameter]
|
||||
public object Data { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "Property")]
|
||||
[Parameter]
|
||||
public PropertyInfo Property { get; set; }
|
||||
|
||||
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
|
||||
</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="/admin/store/expired">
|
||||
<i class="bx bx-sm bx-timer me-2"></i> Expired services
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -84,6 +84,17 @@
|
|||
</a>
|
||||
</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">
|
||||
<a class="menu-link " href="/admin/store">
|
||||
<span class="menu-icon">
|
||||
|
|
|
@ -128,7 +128,13 @@ else
|
|||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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>
|
||||
</h3>
|
||||
</div>
|
||||
|
@ -177,14 +183,28 @@ else
|
|||
[Parameter]
|
||||
public Func<Task> OnChange { get; set; }
|
||||
|
||||
// Renew access state
|
||||
private bool NeedsRenewal = false;
|
||||
|
||||
// States
|
||||
private bool ShowDeletionScreen = false;
|
||||
private bool ShowRenewScreen = false;
|
||||
private ManageServiceShareModal ShareModal;
|
||||
|
||||
// Renewing
|
||||
private int DurationMultiplier = 1;
|
||||
private bool CanBeRenewed = false;
|
||||
private bool IsValidating = false;
|
||||
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()
|
||||
{
|
||||
|
|
|
@ -109,14 +109,13 @@
|
|||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<SmartEnumSelect @bind-Value="AddProductForm.Type"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Config</label>
|
||||
<input @bind="AddProductForm.ConfigJson" class="form-control" type="text"/>
|
||||
<SmartEnumSelect @bind-Value="AddProductServiceType"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<DynamicTypedAutoForm Model="AddProductConfig" Columns="6"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
|
@ -172,14 +171,13 @@
|
|||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<SmartEnumSelect @bind-Value="EditProductForm.Type"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Config</label>
|
||||
<input @bind="EditProductForm.ConfigJson" class="form-control" type="text"/>
|
||||
<SmartEnumSelect @bind-Value="EditProductServiceType"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<DynamicTypedAutoForm Model="EditProductConfig" Columns="6"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
|
@ -231,6 +229,7 @@
|
|||
EditCategoryForm = Mapper.Map<EditCategoryForm>(EditCategory);
|
||||
await EditCategoryModal.Show();
|
||||
}
|
||||
|
||||
private async Task EditCategorySubmit()
|
||||
{
|
||||
EditCategory = Mapper.Map(EditCategory, EditCategoryForm);
|
||||
|
@ -250,17 +249,30 @@
|
|||
private SmartModal AddProductModal;
|
||||
private AddProductForm AddProductForm = new();
|
||||
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();
|
||||
|
||||
private async Task AddProductSubmit()
|
||||
{
|
||||
await StoreService.Admin.AddProduct(
|
||||
var product = await StoreService.Admin.AddProduct(
|
||||
AddProductForm.Name,
|
||||
AddProductForm.Description,
|
||||
AddProductForm.Slug,
|
||||
AddProductForm.Type,
|
||||
AddProductForm.ConfigJson,
|
||||
product =>
|
||||
{
|
||||
product.Category = AddProductForm.Category;
|
||||
|
@ -271,6 +283,8 @@
|
|||
}
|
||||
);
|
||||
|
||||
StoreService.Admin.SaveProductConfig(product, AddProductConfig);
|
||||
|
||||
await ToastService.Success("Successfully added product");
|
||||
await AddProductModal.Hide();
|
||||
|
||||
|
@ -285,10 +299,25 @@
|
|||
private SmartModal EditProductModal;
|
||||
private EditProductForm EditProductForm = new();
|
||||
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)
|
||||
{
|
||||
EditProduct = product;
|
||||
EditProductConfig = StoreService.Admin.GetProductConfig(product);
|
||||
|
||||
EditProductForm = Mapper.Map<EditProductForm>(EditProduct);
|
||||
await EditProductModal.Show();
|
||||
|
@ -299,6 +328,7 @@
|
|||
EditProduct = Mapper.Map(EditProduct, EditProductForm);
|
||||
|
||||
await StoreService.Admin.UpdateProduct(EditProduct);
|
||||
StoreService.Admin.SaveProductConfig(EditProduct, EditProductConfig);
|
||||
|
||||
await ToastService.Success("Successfully updated product");
|
||||
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)
|
||||
{
|
||||
<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)
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
@rf
|
||||
</div>
|
||||
}
|
||||
</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)
|
||||
{
|
||||
var needsRenewal = SharedRenewalStates[service];
|
||||
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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>
|
||||
</h3>
|
||||
</div>
|
||||
|
@ -43,7 +51,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -57,10 +65,19 @@
|
|||
|
||||
private Service[] MyServices;
|
||||
private Service[] SharedServices;
|
||||
private Dictionary<Service, bool> SharedRenewalStates = new();
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
// Load all services
|
||||
MyServices = await ServiceService.Get(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 |
Loading…
Reference in a new issue