Merge pull request #338 from Moonlight-Panel/AddServiceImplementationApi

Added service implementation api and some service utils
This commit is contained in:
Marcel Baumgartner 2023-11-15 21:06:02 +01:00 committed by GitHub
commit 7145890801
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 846 additions and 86 deletions

View file

@ -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;

View 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; }
}

View 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;
}
}

View file

@ -0,0 +1,5 @@
<h3>DummyAdmin</h3>
@code {
}

View file

@ -0,0 +1,5 @@
<h3>DummyUser</h3>
@code {
}

View file

@ -0,0 +1,5 @@
<h3>DummyPage</h3>
@code {
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View 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; }
}

View file

@ -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;
}
}

View file

@ -10,6 +10,7 @@ public enum Permission
AdminUsersEdit = 1003,
AdminTickets = 1004,
AdminCommunity = 1030,
AdminServices = 1050,
AdminStore = 1900,
AdminViewExceptions = 1999,
AdminRoot = 2000

View file

@ -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;
}

View file

@ -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)
{

View file

@ -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>();

View file

@ -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;
}
}

View file

@ -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];
}
}

View 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);
}
}

View file

@ -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)
{

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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);
}
}

View file

@ -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);

View 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>

View file

@ -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>
}

View file

@ -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;

View 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;
}

View file

@ -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>

View file

@ -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">

View file

@ -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()
{

View file

@ -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();

View 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;
}
}

View 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);
}
}

View 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;
}
}

View file

@ -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>

View 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);
}
}

View file

@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB