Refactored some forms. Added service shares

This commit is contained in:
Marcel Baumgartner 2023-10-22 21:28:15 +02:00
parent 48c95d4ec6
commit 3d4f22f6f6
21 changed files with 347 additions and 50 deletions

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Admin.Users;
public class UpdateUserForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Admin.Users;
public class UpdateUserPasswordForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class LoginForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class RegisterForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class ResetPasswordForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class TwoFactorCodeForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class UpdateAccountForm
{

View file

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
namespace Moonlight.App.Models.Forms.Auth;
public class UpdateAccountPasswordForm
{

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Services;
public class AddUserToServiceForm
{
[Required(ErrorMessage = "You need to provide an username")]
[MinLength(7, ErrorMessage = "The username is too short")]
[MaxLength(20, ErrorMessage = "The username cannot be longer than 20 characters")]
[RegularExpression("^[a-z][a-z0-9]*$",
ErrorMessage = "Usernames can only contain lowercase characters and numbers")]
public string Username { get; set; } = "";
}

View file

@ -55,6 +55,34 @@ public class ServiceAdminService
return finishedService;
}
public async Task Delete(Service s)
{
using var scope = ServiceScopeFactory.CreateScope();
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
var serviceShareRepo = scope.ServiceProvider.GetRequiredService<Repository<ServiceShare>>();
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");
await Actions[service.Product.Type].Delete(scope.ServiceProvider, service);
foreach (var share in service.Shares)
{
serviceShareRepo.Delete(share);
}
serviceRepo.Delete(service);
}
public Task RegisterAction(ServiceType type, ServiceActions actions) // Use this function to register service types
{
Actions.Add(type, actions);

View file

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.ServiceManage;
@ -9,13 +10,15 @@ public class ServiceService // This service is used for managing services and cr
{
private readonly IServiceProvider ServiceProvider;
private readonly Repository<Service> ServiceRepository;
private readonly Repository<User> UserRepository;
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository)
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository, Repository<User> userRepository)
{
ServiceProvider = serviceProvider;
ServiceRepository = serviceRepository;
UserRepository = userRepository;
}
public Task<Service[]> Get(User user)
@ -40,4 +43,70 @@ public class ServiceService // This service is used for managing services and cr
return Task.FromResult(result);
}
public Task AddSharedUser(Service s, string username)
{
var userToAdd = UserRepository
.Get()
.FirstOrDefault(x => x.Username == username);
if (userToAdd == null)
throw new DisplayException("No user found with this username");
var service = ServiceRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.Shares)
.ThenInclude(x => x.User)
.First(x => x.Id == s.Id);
if (service.Owner.Id == userToAdd.Id)
throw new DisplayException("The owner cannot be added as a shared user");
if (service.Shares.Any(x => x.User.Id == userToAdd.Id))
throw new DisplayException("The user has already access to this service");
service.Shares.Add(new ()
{
User = userToAdd
});
ServiceRepository.Update(service);
return Task.CompletedTask;
}
public Task<User[]> GetSharedUsers(Service s)
{
var service = ServiceRepository
.Get()
.Include(x => x.Shares)
.ThenInclude(x => x.User)
.First(x => x.Id == s.Id);
var result = service.Shares
.Select(x => x.User)
.ToArray();
return Task.FromResult(result);
}
public Task RemoveSharedUser(Service s, User user)
{
var service = ServiceRepository
.Get()
.Include(x => x.Shares)
.ThenInclude(x => x.User)
.First(x => x.Id == s.Id);
var shareToRemove = service.Shares.FirstOrDefault(x => x.User.Id == user.Id);
if (shareToRemove == null)
throw new DisplayException("This user does not have access to this service");
service.Shares.Remove(shareToRemove);
ServiceRepository.Update(service);
return Task.CompletedTask;
}
}

View file

@ -2,6 +2,7 @@
@using Moonlight.App.Services.Users
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Enums
@using Moonlight.App.Models.Forms.Auth
@inject IdentityService IdentityService
@inject UserService UserService

View file

@ -3,6 +3,7 @@
@using Moonlight.App.Services
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Forms.Auth
@inject IdentityService IdentityService
@inject CookieService CookieService

View file

@ -2,6 +2,7 @@
@using Moonlight.App.Services.Users
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Forms.Auth
@inject UserService UserService

View file

@ -5,6 +5,7 @@
@using Moonlight.App.Models.Forms
@using Moonlight.App.Services.Users
@using Moonlight.App.Exceptions
@using Moonlight.App.Models.Forms.Auth
@inject IdentityService IdentityService
@inject UserService UserService

View file

@ -0,0 +1,105 @@
@using Moonlight.App.Services.ServiceManage
@using Moonlight.App.Database.Entities.Store
@using BlazorTable
@using Moonlight.App.Models.Forms.Services
@inject ServiceService ServiceService
<SmartModal @ref="Modal" CssClasses="modal-dialog-centered modal-lg">
<div class="modal-header">
<h5 class="modal-title">Manage shared users</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="mb-3">
<SmartForm Model="Form" OnValidSubmit="Add">
<div class="input-group">
<input @bind="Form.Username" type="text" placeholder="Enter a username..." class="form-control"/>
<button type="submit" class="btn btn-primary">Add</button>
</div>
</SmartForm>
</div>
@if (Users.Any())
{
<Table TableItem="User"
Items="Users"
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="User" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<div class="symbol symbol-50px me-5">
@if (context.Avatar == null)
{
<img src="/assets/img/avatar.png" alt="Avatar">
}
else
{
<img src="/api/bucket/avatars/@(context.Avatar)" alt="Avatar">
}
</div>
<span class="ms-3">@(context.Username)</span>
</Template>
</Column>
<Column TableItem="User" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<div class="text-end me-3">
<a @onclick="() => Remove(context)" @onclick:preventDefault href="#" class="text-danger">Remove</a>
</div>
</Template>
</Column>
</Table>
}
else
{
<div class="d-flex justify-content-center py-4">
<span class="mt-3 fs-5">No users found</span>
</div>
}
</LazyLoader>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</SmartModal>
@code
{
[Parameter]
public Service Service { get; set; }
private SmartModal Modal;
private LazyLoader LazyLoader;
private User[] Users;
private AddUserToServiceForm Form = new();
private async Task Load(LazyLoader _)
{
Users = await ServiceService.GetSharedUsers(Service);
}
private async Task Add()
{
await ServiceService.AddSharedUser(Service, Form.Username);
Form = new();
await LazyLoader.Reload();
}
private async Task Remove(User user)
{
await ServiceService.RemoveSharedUser(Service, user);
await LazyLoader.Reload();
}
public async Task Show()
{
await Modal.Show();
}
public async Task Hide()
{
await Modal.Hide();
}
}

View file

@ -3,7 +3,10 @@
<div class="modal fade" id="modal@(Id)" tabindex="-1">
<div class="modal-dialog @(CssClasses)">
<div class="modal-content">
@ChildContent
@if (ShouldShow)
{
@ChildContent
}
</div>
</div>
</div>
@ -17,6 +20,7 @@
public RenderFragment ChildContent { get; set; }
private int Id;
private bool ShouldShow = false;
protected override void OnInitialized()
{
@ -25,11 +29,17 @@
public async Task Show()
{
ShouldShow = true;
await InvokeAsync(StateHasChanged);
await ModalService.Show("modal" + Id);
}
public async Task Hide()
{
await ModalService.Hide("modal" + Id);
ShouldShow = false;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -0,0 +1,101 @@
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Services
@using Moonlight.App.Services.ServiceManage
@using Moonlight.Shared.Components.Modals
@inject ConfigService ConfigService
@inject ToastService ToastService
@inject ServiceService ServiceService
@if (ShowDeletionScreen)
{
<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">Do you really wan to delete @(Service.Nickname ?? $"Service {Service.Id}")</span>
</h3>
</div>
<div class="card-body">
<p class="text-gray-400 fs-5 fw-semibold">
This action cannot be undone. Your service data will be deleted and cannot be restored
</p>
</div>
<div class="card-footer p-3">
<div class="btn-group w-100">
<WButton OnClick="Delete" Text="Delete" CssClasses="btn btn-danger w-50 me-3" />
<button @onclick="() => SetShowDeletion(false)" class="btn btn-secondary w-50">Cancel</button>
</div>
</div>
</div>
}
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="text-gray-400 mt-1 fw-semibold fs-6">@(Service.Product.Name)</span>
</h3>
</div>
<div class="card-body fs-6">
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Renew price</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(ConfigService.Get().Store.Currency) @(Service.Product.Price)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Renew at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(Service.RenewAt))</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Created at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(Service.CreatedAt))</span>
</div>
</div>
</div>
<div class="card-footer p-3">
<div class="btn-group w-100 mb-3">
<a href="/service/@(Service.Id)" class="btn btn-primary w-50 me-3">Manage</a>
<button @onclick="() => ShareModal.Show()" class="btn btn-secondary w-50">Manage shares</button>
</div>
<div class="btn-group w-100">
<button class="btn btn-warning w-50 me-3">Renew</button>
<button @onclick="() => SetShowDeletion(true)" class="btn btn-danger w-50">Delete</button>
</div>
</div>
</div>
<ManageServiceShareModal @ref="ShareModal" Service="Service" />
}
@code
{
[Parameter]
public Service Service { get; set; }
[Parameter]
public Func<Task> OnChange { get; set; }
private bool ShowDeletionScreen = false;
private ManageServiceShareModal ShareModal;
private async Task SetShowDeletion(bool b)
{
ShowDeletionScreen = b;
await InvokeAsync(StateHasChanged);
}
public async Task Delete()
{
await ServiceService.Admin.Delete(Service);
await ToastService.Success("Successfully deleted service");
await OnChange.Invoke();
}
}

View file

@ -3,6 +3,7 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Users
@using Moonlight.App.Models.Forms
@using Moonlight.App.Models.Forms.Auth
@inject IdentityService IdentityService
@inject UserService UserService

View file

@ -6,6 +6,7 @@
@using OtpNet
@using QRCoder
@using Moonlight.App.Models.Enums
@using Moonlight.App.Models.Forms.Auth
@inject IdentityService IdentityService
@inject UserService UserService

View file

@ -3,54 +3,17 @@
@using Moonlight.App.Services.ServiceManage
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Service
@inject ServiceService ServiceService
@inject IdentityService IdentityService
@inject ConfigService ConfigService
<LazyLoader ShowAsCard="true" Load="Load">
<LazyLoader @ref="LazyLoader" ShowAsCard="true" Load="Load">
<div class="row mb-5">
@foreach (var service in MyServices)
{
<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="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
</h3>
</div>
<div class="card-body fs-6">
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Price</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(ConfigService.Get().Store.Currency) @(service.Product.Price)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Renew at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(service.RenewAt))</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Created at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(service.CreatedAt))</span>
</div>
</div>
</div>
<div class="card-footer p-3">
<div class="btn-group w-100 mb-3">
<button class="btn btn-primary w-50 me-3">Manage</button>
<button class="btn btn-secondary w-50">Manage shares</button>
</div>
<div class="btn-group w-100">
<button class="btn btn-warning w-50 me-3">Renew</button>
<button class="btn btn-danger w-50">Delete</button>
</div>
</div>
</div>
<ServiceItem Service="service" OnChange="LazyLoader.Reload" />
</div>
}
@ -90,6 +53,8 @@
@code
{
private LazyLoader LazyLoader;
private Service[] MyServices;
private Service[] SharedServices;