diff --git a/Moonlight/App/Actions/Dummy/DummyActions.cs b/Moonlight/App/Actions/Dummy/DummyActions.cs new file mode 100644 index 00000000..1970ddcb --- /dev/null +++ b/Moonlight/App/Actions/Dummy/DummyActions.cs @@ -0,0 +1,22 @@ +using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Models.Abstractions; + +namespace Moonlight.App.Actions.Dummy; + +public class DummyActions : ServiceActions +{ + public override Task Create(IServiceProvider provider, Service service) + { + return Task.CompletedTask; + } + + public override Task Update(IServiceProvider provider, Service service) + { + return Task.CompletedTask; + } + + public override Task Delete(IServiceProvider provider, Service service) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Store/Product.cs b/Moonlight/App/Database/Entities/Store/Product.cs index 75e3c4f3..df3d6c2e 100644 --- a/Moonlight/App/Database/Entities/Store/Product.cs +++ b/Moonlight/App/Database/Entities/Store/Product.cs @@ -16,7 +16,7 @@ public class Product public int MaxPerUser { get; set; } public int Duration { get; set; } - public ProductType Type { get; set; } + public ServiceType Type { get; set; } public string ConfigJson { get; set; } = "{}"; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/Moonlight/App/Database/Enums/ProductType.cs b/Moonlight/App/Database/Enums/ServiceType.cs similarity index 80% rename from Moonlight/App/Database/Enums/ProductType.cs rename to Moonlight/App/Database/Enums/ServiceType.cs index d7ff0d99..1f2c4d14 100644 --- a/Moonlight/App/Database/Enums/ProductType.cs +++ b/Moonlight/App/Database/Enums/ServiceType.cs @@ -1,6 +1,6 @@ namespace Moonlight.App.Database.Enums; -public enum ProductType +public enum ServiceType { Server, Webspace, diff --git a/Moonlight/App/Models/Abstractions/ServiceActions.cs b/Moonlight/App/Models/Abstractions/ServiceActions.cs new file mode 100644 index 00000000..6ae14d84 --- /dev/null +++ b/Moonlight/App/Models/Abstractions/ServiceActions.cs @@ -0,0 +1,10 @@ +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); +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Store/AddProductForm.cs b/Moonlight/App/Models/Forms/Store/AddProductForm.cs index bb62ffff..a2338ebe 100644 --- a/Moonlight/App/Models/Forms/Store/AddProductForm.cs +++ b/Moonlight/App/Models/Forms/Store/AddProductForm.cs @@ -31,6 +31,6 @@ public class AddProductForm [Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")] public int Duration { get; set; } - public ProductType Type { get; set; } + public ServiceType Type { get; set; } public string ConfigJson { get; set; } = "{}"; } \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Store/EditProductForm.cs b/Moonlight/App/Models/Forms/Store/EditProductForm.cs index 48f53220..1ba410c2 100644 --- a/Moonlight/App/Models/Forms/Store/EditProductForm.cs +++ b/Moonlight/App/Models/Forms/Store/EditProductForm.cs @@ -31,6 +31,6 @@ public class EditProductForm [Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")] public int Duration { get; set; } - public ProductType Type { get; set; } + public ServiceType Type { get; set; } public string ConfigJson { get; set; } = "{}"; } \ No newline at end of file diff --git a/Moonlight/App/Services/ServiceManage/ServiceAdminService.cs b/Moonlight/App/Services/ServiceManage/ServiceAdminService.cs new file mode 100644 index 00000000..b06b717b --- /dev/null +++ b/Moonlight/App/Services/ServiceManage/ServiceAdminService.cs @@ -0,0 +1,63 @@ +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 Actions = new(); + private readonly IServiceScopeFactory ServiceScopeFactory; + + public ServiceAdminService(IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + } + + public async Task Create(User u, Product p, Action? modifyService = null) + { + if (!Actions.ContainsKey(p.Type)) + throw new DisplayException($"The product type {p.Type} is not registered"); + + // Load models in new scope + using var scope = ServiceScopeFactory.CreateScope(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + var productRepo = scope.ServiceProvider.GetRequiredService>(); + var serviceRepo = scope.ServiceProvider.GetRequiredService>(); + + var user = userRepo.Get().First(x => x.Id == u.Id); + var product = productRepo.Get().First(x => x.Id == p.Id); + + // Create database model + var service = new Service() + { + Product = product, + Owner = user, + Suspended = false, + CreatedAt = DateTime.UtcNow + }; + + // Allow further modifications + if(modifyService != null) + modifyService.Invoke(service); + + // Add new service in database + 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); + + return finishedService; + } + + public Task RegisterAction(ServiceType type, ServiceActions actions) // Use this function to register service types + { + Actions.Add(type, actions); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ServiceManage/ServiceService.cs b/Moonlight/App/Services/ServiceManage/ServiceService.cs new file mode 100644 index 00000000..26e917d0 --- /dev/null +++ b/Moonlight/App/Services/ServiceManage/ServiceService.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.ServiceManage; + +public class ServiceService // This service is used for managing services and create the connection to the actual logic behind a service type +{ + private readonly IServiceProvider ServiceProvider; + private readonly Repository ServiceRepository; + + public ServiceAdminService Admin => ServiceProvider.GetRequiredService(); + + public ServiceService(IServiceProvider serviceProvider, Repository serviceRepository) + { + ServiceProvider = serviceProvider; + ServiceRepository = serviceRepository; + } + + public Task Get(User user) + { + var result = ServiceRepository + .Get() + .Include(x => x.Product) + .Where(x => x.Owner.Id == user.Id) + .ToArray(); + + return Task.FromResult(result); + } + + public Task GetShared(User user) + { + var result = ServiceRepository + .Get() + .Include(x => x.Product) + .Include(x => x.Owner) + .Where(x => x.Shares.Any(y => y.User.Id == user.Id)) + .ToArray(); + + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Store/StoreAdminService.cs b/Moonlight/App/Services/Store/StoreAdminService.cs index a8931792..889708c2 100644 --- a/Moonlight/App/Services/Store/StoreAdminService.cs +++ b/Moonlight/App/Services/Store/StoreAdminService.cs @@ -31,7 +31,7 @@ public class StoreAdminService return Task.FromResult(result); } - public Task AddProduct(string name, string description, string slug, ProductType type, string configJson, + public Task AddProduct(string name, string description, string slug, ServiceType type, string configJson, Action? modifyProduct = null) { if (ProductRepository.Get().Any(x => x.Slug == slug)) diff --git a/Moonlight/App/Services/Store/StoreOrderService.cs b/Moonlight/App/Services/Store/StoreOrderService.cs index 3d4e0e06..a92cd91b 100644 --- a/Moonlight/App/Services/Store/StoreOrderService.cs +++ b/Moonlight/App/Services/Store/StoreOrderService.cs @@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Store; using Moonlight.App.Exceptions; using Moonlight.App.Repositories; +using Moonlight.App.Services.ServiceManage; namespace Moonlight.App.Services.Store; @@ -84,4 +85,33 @@ public class StoreOrderService return Task.CompletedTask; } + + public async Task Process(User u, Product p, int durationMultiplier, Coupon? c) + { + // Validate to ensure we dont process an illegal order + await Validate(u, p, durationMultiplier, c); + + // Create scope and get required services + using var scope = ServiceScopeFactory.CreateScope(); + var serviceService = scope.ServiceProvider.GetRequiredService(); + var transactionService = scope.ServiceProvider.GetRequiredService(); + + // Calculate price + var price = p.Price * durationMultiplier; + + if (c != null) + price = Math.Round(price * c.Percent / 100, 2); + + // Calculate duration + var duration = durationMultiplier * p.Duration; + + // Add transaction + await transactionService.Add(u, -1 * price, $"Bought product '{p.Name}' for {duration} days"); + + // Create service + return await serviceService.Admin.Create(u, p, service => + { + service.RenewAt = DateTime.UtcNow.AddDays(duration); + }); + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Store/TransactionService.cs b/Moonlight/App/Services/Store/TransactionService.cs new file mode 100644 index 00000000..78536627 --- /dev/null +++ b/Moonlight/App/Services/Store/TransactionService.cs @@ -0,0 +1,35 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Store; + +public class TransactionService +{ + private readonly Repository UserRepository; + + public TransactionService(Repository userRepository) + { + UserRepository = userRepository; + } + + public Task Add(User u, double amount, string message) + { + var user = UserRepository.Get().First(x => x.Id == u.Id); // Load user with current repo + + user.Transactions.Add(new Transaction() + { + Text = message, + Price = amount + }); + + UserRepository.Update(user); + + // We divide the call to ensure the transaction can be written to the database + + user.Balance += amount; + UserRepository.Update(user); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index f59a6499..3d0d63ea 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,5 +1,7 @@ using BlazorTable; +using Moonlight.App.Actions.Dummy; using Moonlight.App.Database; +using Moonlight.App.Database.Enums; using Moonlight.App.Extensions; using Moonlight.App.Helpers; using Moonlight.App.Helpers.LogMigrator; @@ -7,6 +9,7 @@ using Moonlight.App.Repositories; using Moonlight.App.Services; using Moonlight.App.Services.Background; using Moonlight.App.Services.Interop; +using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.Store; using Moonlight.App.Services.Users; using Moonlight.App.Services.Utils; @@ -44,6 +47,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Services / Users builder.Services.AddScoped(); @@ -53,6 +57,10 @@ builder.Services.AddScoped(); // Services / Background builder.Services.AddSingleton(); +// Services / ServiceManage +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + // Services builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -86,4 +94,7 @@ app.MapControllers(); // Auto start background services app.Services.GetRequiredService(); +var serviceService = app.Services.GetRequiredService(); +await serviceService.RegisterAction(ServiceType.Server, new DummyActions()); + app.Run(); \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/LazyLoader.razor b/Moonlight/Shared/Components/Partials/LazyLoader.razor index 7da0ced0..cc9d917f 100644 --- a/Moonlight/Shared/Components/Partials/LazyLoader.razor +++ b/Moonlight/Shared/Components/Partials/LazyLoader.razor @@ -1,34 +1,49 @@ -@if (loaded) +@if (Loaded) { @ChildContent } else { -
- - @(Text) -
+ if (ShowAsCard) + { +
+
+ + @(Text) +
+
+ } + else + { +
+ + @(Text) +
+ } } @code { [Parameter] public RenderFragment ChildContent { get; set; } - + + [Parameter] + public bool ShowAsCard { get; set; } = false; + [Parameter] public Func Load { get; set; } [Parameter] public string Text { get; set; } = ""; - private bool loaded = false; + private bool Loaded = false; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await Load.Invoke(this); - loaded = true; + Loaded = true; await InvokeAsync(StateHasChanged); } } @@ -41,10 +56,10 @@ else public async Task Reload() { - loaded = false; + Loaded = false; await InvokeAsync(StateHasChanged); await Load.Invoke(this); - loaded = true; + Loaded = true; await InvokeAsync(StateHasChanged); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Services/Index.razor b/Moonlight/Shared/Views/Services/Index.razor new file mode 100644 index 00000000..583df09f --- /dev/null +++ b/Moonlight/Shared/Views/Services/Index.razor @@ -0,0 +1,101 @@ +@page "/services" + +@using Moonlight.App.Services.ServiceManage +@using Moonlight.App.Database.Entities.Store +@using Moonlight.App.Services + +@inject ServiceService ServiceService +@inject IdentityService IdentityService +@inject ConfigService ConfigService + + +
+ @foreach (var service in MyServices) + { +
+
+
+

+ @(service.Nickname ?? $"Service {service.Id}") + @(service.Product.Name) +

+
+
+
+
Price
+
+ @(ConfigService.Get().Store.Currency) @(service.Product.Price) +
+
+
+
Renew at
+
+ @(Formatter.FormatDate(service.RenewAt)) +
+
+
+
Created at
+
+ @(Formatter.FormatDate(service.CreatedAt)) +
+
+
+ +
+
+ } + + @foreach (var service in SharedServices) + { +
+
+
+

+ @(service.Nickname ?? $"Service {service.Id}") + @(service.Product.Name) +

+
+
+
+
Shared by
+
+ @(service.Owner.Username) +
+
+ +
+
Created at
+
+ @(Formatter.FormatDate(service.CreatedAt)) +
+
+
+ +
+
+ } +
+
+ +@code +{ + private Service[] MyServices; + private Service[] SharedServices; + + private async Task Load(LazyLoader _) + { + MyServices = await ServiceService.Get(IdentityService.CurrentUser); + SharedServices = await ServiceService.GetShared(IdentityService.CurrentUser); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Store/Order.razor b/Moonlight/Shared/Views/Store/Order.razor index 4a802dbe..40d4925b 100644 --- a/Moonlight/Shared/Views/Store/Order.razor +++ b/Moonlight/Shared/Views/Store/Order.razor @@ -4,11 +4,14 @@ @using Moonlight.App.Services @using Moonlight.App.Database.Entities.Store @using Moonlight.App.Repositories +@using Moonlight.App.Services.ServiceManage @inject ConfigService ConfigService @inject StoreService StoreService @inject IdentityService IdentityService @inject AlertService AlertService +@inject NavigationManager Navigation +@inject ServiceService ServiceService @inject Repository ProductRepository @inject Repository CouponRepository @@ -73,7 +76,7 @@ TODO: Add 404 here actualPrice = defaultPrice; else actualPrice = Math.Round(defaultPrice * SelectedCoupon.Percent / 100, 2); - + var currency = ConfigService.Get().Store.Currency; } @@ -81,7 +84,7 @@ TODO: Add 404 here
Today
- @(currency) @(defaultPrice) + @(currency) @(actualPrice)
@@ -136,7 +139,7 @@ TODO: Add 404 here { if (CanBeOrdered) { - + } else { @@ -161,7 +164,7 @@ TODO: Add 404 here private int DurationMultiplicator = 1; private string CouponCode = ""; - + private bool CanBeOrdered = false; private bool IsValidating = false; private string ErrorMessage = ""; @@ -227,4 +230,22 @@ TODO: Add 404 here await Revalidate(); } + + private async Task OnSubmit() + { + if (SelectedProduct == null) // Prevent processing null + return; + + // Process the order with the selected values + var service = await StoreService + .Order + .Process( + IdentityService.CurrentUser, + SelectedProduct, + DurationMultiplicator, + SelectedCoupon + ); + + Navigation.NavigateTo("/service/" + service.Id); + } } \ No newline at end of file