Implemented basic user auth, register, login, details and avatar stuff from helio
This commit is contained in:
80
Moonlight/Shared/Components/Auth/Login.razor
Normal file
80
Moonlight/Shared/Components/Auth/Login.razor
Normal file
@@ -0,0 +1,80 @@
|
||||
@page "/login"
|
||||
@* Virtual route to trick blazor *@
|
||||
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Models.Forms
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject CookieService CookieService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Sign In
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
Get unlimited access & earn money
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||
<div class="fv-row mb-8">
|
||||
<input @bind="Form.Email" type="text" placeholder="Email" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-7">
|
||||
<input @bind="Form.Password" type="password" placeholder="Password" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack flex-wrap gap-3 fs-base fw-semibold mb-10">
|
||||
<a href="/reset-password" class="link-primary">
|
||||
Forgot Password ?
|
||||
</a>
|
||||
<a href="/register" class="link-primary">
|
||||
Need an account ?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<button type="submit" class="btn btn-primary me-2 flex-shrink-0">Sign In</button>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="text-gray-400 fw-semibold fs-6 me-3 me-md-6">Or</div>
|
||||
@* OAuth2 Providers here *@
|
||||
</div>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private LoginForm Form = new();
|
||||
|
||||
// 2FA
|
||||
private bool Require2FA = false;
|
||||
private string TwoFactorCode = "";
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
string token;
|
||||
|
||||
try
|
||||
{
|
||||
token = await IdentityService.Login(Form.Email, Form.Password, TwoFactorCode);
|
||||
}
|
||||
catch (ArgumentNullException) // IdentityService requires two factor code => show field
|
||||
{
|
||||
Require2FA = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
await CookieService.SetValue("token", token);
|
||||
await IdentityService.Authenticate(token);
|
||||
|
||||
if (Navigation.Uri.EndsWith("/login"))
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
78
Moonlight/Shared/Components/Auth/Register.razor
Normal file
78
Moonlight/Shared/Components/Auth/Register.razor
Normal file
@@ -0,0 +1,78 @@
|
||||
@page "/register"
|
||||
@* Virtual route to trick blazor *@
|
||||
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Exceptions
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
@inject CookieService CookieService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Sign Up
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
Get unlimited access & earn money
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||
<div class="fv-row mb-8">
|
||||
<input @bind="Form.Username" type="text" placeholder="Username" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-8">
|
||||
<input @bind="Form.Email" type="text" placeholder="Email" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-7">
|
||||
<input @bind="Form.Password" type="password" placeholder="Password" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-7">
|
||||
<input @bind="Form.RepeatedPassword" type="password" placeholder="Repeat your password" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack flex-wrap gap-3 fs-base fw-semibold mb-10">
|
||||
<div></div>
|
||||
<a href="/login" class="link-primary">
|
||||
Already have an account ?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<button type="submit" class="btn btn-primary me-2 flex-shrink-0">Sign Up</button>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="text-gray-400 fw-semibold fs-6 me-3 me-md-6">Or</div>
|
||||
@* OAuth2 Providers here *@
|
||||
</div>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private RegisterForm Form = new();
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
if (Form.Password != Form.RepeatedPassword)
|
||||
throw new DisplayException("The passwords do not match");
|
||||
|
||||
var user = await UserService.Auth.Register(Form.Username, Form.Email, Form.Password);
|
||||
var token = await IdentityService.GenerateToken(user);
|
||||
|
||||
await CookieService.SetValue("token", token);
|
||||
await IdentityService.Authenticate(token);
|
||||
|
||||
if (Navigation.Uri.EndsWith("/register"))
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
57
Moonlight/Shared/Components/Forms/SmartFileSelect.razor
Normal file
57
Moonlight/Shared/Components/Forms/SmartFileSelect.razor
Normal file
@@ -0,0 +1,57 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
@inject ToastService ToastService
|
||||
|
||||
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden=""/>
|
||||
<label for="fileUpload" class="">
|
||||
@if (SelectedFile != null)
|
||||
{
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control disabled" value="@(SelectedFile.Name)" disabled="">
|
||||
<button class="btn btn-danger" type="button" @onclick="RemoveSelection">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="btn btn-primary me-3 btn-icon">
|
||||
<i class="bx bx-upload"></i>
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public IBrowserFile? SelectedFile { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int MaxFileSize { get; set; } = 1024 * 1024 * 5;
|
||||
|
||||
private async Task OnFileChanged(InputFileChangeEventArgs arg)
|
||||
{
|
||||
if (arg.FileCount > 0)
|
||||
{
|
||||
if (arg.File.Size < 1024 * 1024 * 5)
|
||||
{
|
||||
SelectedFile = arg.File;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
await ToastService.Danger("The uploaded file should not be bigger than 5MB");
|
||||
}
|
||||
|
||||
SelectedFile = null;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task RemoveSelection()
|
||||
{
|
||||
SelectedFile = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
132
Moonlight/Shared/Components/Forms/SmartForm.razor
Normal file
132
Moonlight/Shared/Components/Forms/SmartForm.razor
Normal file
@@ -0,0 +1,132 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Moonlight.App.Exceptions
|
||||
|
||||
<div class="form @CssClass">
|
||||
<EditForm @ref="EditForm" Model="Model" OnValidSubmit="ValidSubmit" OnInvalidSubmit="InvalidSubmit">
|
||||
<DataAnnotationsValidator></DataAnnotationsValidator>
|
||||
@if (Working)
|
||||
{
|
||||
<div class="d-flex flex-center flex-column">
|
||||
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
|
||||
<span class="mt-3 fs-5">Proccessing</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ErrorMessages.Any())
|
||||
{
|
||||
<div class="alert alert-danger bg-danger text-white p-10 mb-3">
|
||||
@foreach (var msg in ErrorMessages)
|
||||
{
|
||||
<TL>@(msg)</TL>
|
||||
<br/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@(ChildContent)
|
||||
}
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public object Model { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task>? OnValidSubmit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task>? OnInvalidSubmit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task>? OnSubmit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; }
|
||||
|
||||
private EditForm EditForm;
|
||||
|
||||
private List<string> ErrorMessages = new();
|
||||
|
||||
private bool Working = false;
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
//EditForm.EditContext!.SetFieldCssClassProvider(new FieldCssHelper());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidSubmit(EditContext context)
|
||||
{
|
||||
ErrorMessages.Clear();
|
||||
Working = true;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OnValidSubmit != null)
|
||||
await OnValidSubmit.Invoke();
|
||||
|
||||
if (OnSubmit != null)
|
||||
await OnSubmit.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is DisplayException displayException)
|
||||
{
|
||||
ErrorMessages.Add(displayException.Message);
|
||||
}
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
Working = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InvalidSubmit(EditContext context)
|
||||
{
|
||||
ErrorMessages.Clear();
|
||||
context.Validate();
|
||||
|
||||
foreach (var message in context.GetValidationMessages())
|
||||
{
|
||||
ErrorMessages.Add(message);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
if (OnInvalidSubmit != null)
|
||||
await OnInvalidSubmit.Invoke();
|
||||
|
||||
if (OnSubmit != null)
|
||||
await OnSubmit.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is DisplayException displayException)
|
||||
{
|
||||
ErrorMessages.Add(displayException.Message);
|
||||
}
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="card mb-5 mb-xl-10">
|
||||
<div class="card-body pt-0 pb-0">
|
||||
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/account">
|
||||
General
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/account/security">
|
||||
Security
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public int Index { get; set; }
|
||||
}
|
||||
@@ -1,213 +1,70 @@
|
||||
@using Moonlight.Shared.Layouts
|
||||
<div id="kt_app_header" class="app-header ">
|
||||
<div class="app-container container-fluid d-flex align-items-stretch flex-stack " id="kt_app_header_container">
|
||||
<div class="d-flex align-items-center d-block d-lg-none ms-n3" title="Show sidebar menu">
|
||||
<a href="#" @onclick="Layout.ToggleMobileSidebar" @onclick:preventDefault class="btn btn-icon btn-active-color-primary w-35px h-35px me-2">
|
||||
<i class="bx bx-sm bx-menu"></i>
|
||||
</a>
|
||||
<a href="/metronic8/demo38/../demo38/index.html">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/demo38-small.svg" class="h-30px">
|
||||
</a>
|
||||
</div>
|
||||
<div class="app-navbar flex-lg-grow-1" id="kt_app_header_navbar">
|
||||
<div class="app-navbar-item d-flex align-items-stretch flex-lg-grow-1">
|
||||
</div>
|
||||
|
||||
<div class="app-navbar-item ms-1 ms-md-3">
|
||||
<ConnectionIndicator />
|
||||
</div>
|
||||
|
||||
<div class="app-navbar-item ms-5 ms-md-5">
|
||||
<div class="cursor-pointer symbol symbol-circle symbol-35px symbol-md-45px" id="dropdownMenuLink" data-bs-toggle="dropdown">
|
||||
<img src="https://endelon-hosting.de/assets/img/logo.svg" alt="user">
|
||||
@using Moonlight.App.Services
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject CookieService CookieService
|
||||
|
||||
<div class="app-header">
|
||||
<div class="app-container container-fluid d-flex align-items-stretch flex-stack">
|
||||
<div class="d-flex align-items-center d-block d-lg-none ms-n3" title="Show sidebar menu">
|
||||
<a href="#" @onclick="Layout.ToggleMobileSidebar" @onclick:preventDefault class="btn btn-icon btn-active-color-primary w-35px h-35px me-2">
|
||||
<i class="bx bx-sm bx-menu"></i>
|
||||
</a>
|
||||
<a href="/metronic8/demo38/../demo38/index.html">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/demo38-small.svg" class="h-30px">
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-menu dropdown-menu-end menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg menu-state-color fw-semibold py-4 fs-6 w-275px" aria-labelledby="dropdownMenuLink">
|
||||
<div class="menu-item px-3">
|
||||
<div class="menu-content d-flex align-items-center px-3">
|
||||
<div class="symbol symbol-50px me-5">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/avatars/300-2.jpg">
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="fw-bold d-flex align-items-center fs-5">
|
||||
Max Smith <span class="badge badge-light-success fw-bold fs-8 px-2 py-1 ms-2">Pro</span>
|
||||
</div>
|
||||
<a href="#" class="fw-semibold text-muted text-hover-primary fs-7">
|
||||
max@kt.com
|
||||
</a>
|
||||
</div>
|
||||
<div class="app-navbar flex-lg-grow-1">
|
||||
<div class="app-navbar-item d-flex align-items-stretch flex-lg-grow-1">
|
||||
</div>
|
||||
|
||||
<div class="app-navbar-item ms-1 ms-md-3">
|
||||
<ConnectionIndicator/>
|
||||
</div>
|
||||
|
||||
<div class="app-navbar-item ms-5 ms-md-5">
|
||||
<div class="cursor-pointer symbol symbol-circle symbol-35px symbol-md-45px" id="dropdownMenuLink" data-bs-toggle="dropdown">
|
||||
@if (IdentityService.CurrentUser.Avatar == null)
|
||||
{
|
||||
<img src="/assets/img/avatar.png" alt="Avatar">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/api/bucket/avatars/@(IdentityService.CurrentUser.Avatar)" alt="Avatar">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/metronic8/demo38/../demo38/account/overview.html" class="menu-link px-5">
|
||||
My Profile
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/metronic8/demo38/../demo38/apps/projects/list.html" class="menu-link px-5">
|
||||
<span class="menu-text">My Projects</span>
|
||||
<span class="menu-badge">
|
||||
<span class="badge badge-light-danger badge-circle fw-bold fs-7">3</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-5" data-kt-menu-trigger="{default: 'click', lg: 'hover'}" data-kt-menu-placement="left-start" data-kt-menu-offset="-15px, 0">
|
||||
<a href="#" class="menu-link px-5">
|
||||
<span class="menu-title">My Subscription</span>
|
||||
<span class="menu-arrow"></span>
|
||||
</a>
|
||||
<div class="menu-sub menu-sub-dropdown w-175px py-4">
|
||||
<div class="dropdown-menu dropdown-menu-end menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg menu-state-color fw-semibold py-4 fs-6 w-275px" aria-labelledby="dropdownMenuLink">
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/referrals.html" class="menu-link px-5">
|
||||
Referrals
|
||||
</a>
|
||||
<div class="menu-content d-flex align-items-center px-3">
|
||||
<div class="symbol symbol-50px me-5">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/avatars/300-2.jpg">
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="fw-bold d-flex align-items-center fs-5">
|
||||
@(IdentityService.CurrentUser.Username)
|
||||
</div>
|
||||
<span class="fw-semibold text-muted fs-7">
|
||||
@(IdentityService.CurrentUser.Email)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/billing.html" class="menu-link px-5">
|
||||
Billing
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/statements.html" class="menu-link px-5">
|
||||
Payments
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/statements.html" class="menu-link d-flex flex-stack px-5">
|
||||
Statements
|
||||
<span class="ms-2 lh-0" data-bs-toggle="tooltip" aria-label="View your statements" data-bs-original-title="View your statements" data-kt-initialized="1">
|
||||
<i class="ki-outline ki-information-5 fs-5"></i>
|
||||
</span>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/account" class="menu-link px-5">
|
||||
Account
|
||||
</a>
|
||||
</div>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-3">
|
||||
<div class="menu-content px-3">
|
||||
<label class="form-check form-switch form-check-custom form-check-solid">
|
||||
<input class="form-check-input w-30px h-20px" type="checkbox" value="1" checked="checked" name="notifications">
|
||||
<span class="form-check-label text-muted fs-7">
|
||||
Notifications
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/metronic8/demo38/../demo38/account/statements.html" class="menu-link px-5">
|
||||
My Statements
|
||||
</a>
|
||||
</div>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-5" data-kt-menu-trigger="{default: 'click', lg: 'hover'}" data-kt-menu-placement="left-start" data-kt-menu-offset="-15px, 0">
|
||||
<a href="#" class="menu-link px-5">
|
||||
<span class="menu-title position-relative">
|
||||
Mode
|
||||
|
||||
<span class="ms-5 position-absolute translate-middle-y top-50 end-0">
|
||||
<i class="ki-outline ki-night-day theme-light-show fs-2"></i> <i class="ki-outline ki-moon theme-dark-show fs-2"></i>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 fs-base w-150px" data-kt-menu="true" data-kt-element="theme-mode-menu">
|
||||
<div class="menu-item px-3 my-0">
|
||||
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="light">
|
||||
<span class="menu-icon" data-kt-element="icon">
|
||||
<i class="ki-outline ki-night-day fs-2"></i>
|
||||
</span>
|
||||
<span class="menu-title">
|
||||
Light
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3 my-0">
|
||||
<a href="#" class="menu-link px-3 py-2 active" data-kt-element="mode" data-kt-value="dark">
|
||||
<span class="menu-icon" data-kt-element="icon">
|
||||
<i class="ki-outline ki-moon fs-2"></i>
|
||||
</span>
|
||||
<span class="menu-title">
|
||||
Dark
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3 my-0">
|
||||
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="system">
|
||||
<span class="menu-icon" data-kt-element="icon">
|
||||
<i class="ki-outline ki-screen fs-2"></i>
|
||||
</span>
|
||||
<span class="menu-title">
|
||||
System
|
||||
</span>
|
||||
<div class="menu-item px-5">
|
||||
<a href="#" @onclick="Logout" @onclick:preventDefault class="menu-link px-5">
|
||||
Sign Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-5" data-kt-menu-trigger="{default: 'click', lg: 'hover'}" data-kt-menu-placement="left-start" data-kt-menu-offset="-15px, 0">
|
||||
<a href="#" class="menu-link px-5">
|
||||
<span class="menu-title position-relative">
|
||||
Language
|
||||
<span class="fs-8 rounded bg-light px-3 py-2 position-absolute translate-middle-y top-50 end-0">
|
||||
English <img class="w-15px h-15px rounded-1 ms-2" src="/metronic8/demo38/assets/media/flags/united-states.svg" alt="">
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="menu-sub menu-sub-dropdown w-175px py-4">
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link d-flex px-5 active">
|
||||
<span class="symbol symbol-20px me-4">
|
||||
<img class="rounded-1" src="/metronic8/demo38/assets/media/flags/united-states.svg" alt="">
|
||||
</span>
|
||||
English
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link d-flex px-5">
|
||||
<span class="symbol symbol-20px me-4">
|
||||
<img class="rounded-1" src="/metronic8/demo38/assets/media/flags/spain.svg" alt="">
|
||||
</span>
|
||||
Spanish
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link d-flex px-5">
|
||||
<span class="symbol symbol-20px me-4">
|
||||
<img class="rounded-1" src="/metronic8/demo38/assets/media/flags/germany.svg" alt="">
|
||||
</span>
|
||||
German
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link d-flex px-5">
|
||||
<span class="symbol symbol-20px me-4">
|
||||
<img class="rounded-1" src="/metronic8/demo38/assets/media/flags/japan.svg" alt="">
|
||||
</span>
|
||||
Japanese
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link d-flex px-5">
|
||||
<span class="symbol symbol-20px me-4">
|
||||
<img class="rounded-1" src="/metronic8/demo38/assets/media/flags/france.svg" alt="">
|
||||
</span>
|
||||
French
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item px-5 my-1">
|
||||
<a href="/metronic8/demo38/../demo38/account/settings.html" class="menu-link px-5">
|
||||
Account Settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/metronic8/demo38/../demo38/authentication/layouts/corporate/sign-in.html" class="menu-link px-5">
|
||||
Sign Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -216,4 +73,10 @@
|
||||
{
|
||||
[CascadingParameter]
|
||||
public DefaultLayout Layout { get; set; }
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
await IdentityService.Authenticate("");
|
||||
await CookieService.SetValue("token", "");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@using System.Diagnostics
|
||||
@using Moonlight.App.Exceptions
|
||||
|
||||
@inherits ErrorBoundaryBase
|
||||
|
||||
@if (Crashed)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="d-flex flex-column flex-root app-root">
|
||||
<div class="app-page flex-column flex-column-fluid">
|
||||
<CascadingValue Value="this">
|
||||
<PageHeader/>
|
||||
<PageHeader />
|
||||
<div class="app-wrapper flex-column flex-row-fluid">
|
||||
<Sidebar/>
|
||||
<div class="d-flex flex-column flex-column-fluid">
|
||||
@@ -25,9 +25,8 @@
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
public bool ShowMobileSidebar { get; set; }
|
||||
|
||||
|
||||
public async Task ToggleMobileSidebar()
|
||||
{
|
||||
ShowMobileSidebar = !ShowMobileSidebar;
|
||||
|
||||
@@ -1,7 +1,132 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Models.Abstractions
|
||||
@using Moonlight.App.Models.Enums
|
||||
@using Moonlight.Shared.Components.Auth
|
||||
|
||||
<DefaultLayout>
|
||||
<SoftErrorHandler>
|
||||
@Body
|
||||
</SoftErrorHandler>
|
||||
</DefaultLayout>
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
|
||||
@inject CookieService CookieService
|
||||
@inject ConfigService ConfigService
|
||||
@inject IdentityService IdentityService
|
||||
@inject SessionService SessionService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@{
|
||||
var url = new Uri(Navigation.Uri);
|
||||
}
|
||||
|
||||
@if (Initialized)
|
||||
{
|
||||
if (IdentityService.IsSignedIn)
|
||||
{
|
||||
if (!IdentityService.Flags[UserFlag.MailVerified] && ConfigService.Get().Security.EnableEmailVerify)
|
||||
{
|
||||
}
|
||||
else if (IdentityService.Flags[UserFlag.PasswordPending])
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
<DefaultLayout>
|
||||
<SoftErrorHandler>
|
||||
@Body
|
||||
</SoftErrorHandler>
|
||||
</DefaultLayout>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (url.LocalPath == "/register")
|
||||
{
|
||||
<OverlayLayout>
|
||||
<Register />
|
||||
</OverlayLayout>
|
||||
}
|
||||
else
|
||||
{
|
||||
<OverlayLayout>
|
||||
<Login />
|
||||
</OverlayLayout>
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<OverlayLayout>
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-dark mb-3 fs-4">
|
||||
Connecting to the remote server
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
<div class="text-success">
|
||||
<div class="spinner-border me-2" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayLayout>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
private bool Initialized = false;
|
||||
|
||||
private Session? MySession;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
IdentityService.OnAuthenticationStateChanged += async (_, _) =>
|
||||
{
|
||||
if (MySession != null)
|
||||
{
|
||||
MySession.User = IdentityService.CurrentUserNullable;
|
||||
MySession.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
};
|
||||
|
||||
Navigation.LocationChanged += (_, _) =>
|
||||
{
|
||||
if (MySession != null)
|
||||
{
|
||||
MySession.Url = Navigation.Uri;
|
||||
MySession.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var token = await CookieService.GetValue("token");
|
||||
await IdentityService.Authenticate(token);
|
||||
|
||||
MySession = new()
|
||||
{
|
||||
//Ip = ConnectionService.GetIp(), TODO: Implement
|
||||
Url = Navigation.Uri,
|
||||
User = IdentityService.CurrentUserNullable
|
||||
};
|
||||
|
||||
await SessionService.Register(MySession);
|
||||
|
||||
Initialized = true;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Dispose() // This method will be called if the user closes his tab
|
||||
{
|
||||
if (MySession != null)
|
||||
await SessionService.Unregister(MySession);
|
||||
}
|
||||
}
|
||||
27
Moonlight/Shared/Layouts/OverlayLayout.razor
Normal file
27
Moonlight/Shared/Layouts/OverlayLayout.razor
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="d-flex flex-column flex-root" id="kt_app_root">
|
||||
<div class="d-flex flex-column flex-lg-row flex-column-fluid">
|
||||
<a href="/" class="d-block d-lg-none mx-auto py-20">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/default.svg" class="theme-light-show h-25px">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/default-dark.svg" class="theme-dark-show h-25px">
|
||||
</a>
|
||||
<div class="d-flex flex-column flex-column-fluid flex-center w-lg-50 p-10">
|
||||
<div class="d-flex justify-content-between flex-column-fluid flex-column w-100 mw-450px">
|
||||
<div class="d-flex flex-stack py-2">
|
||||
</div>
|
||||
<div class="py-20">
|
||||
@ChildContent
|
||||
</div>
|
||||
<div class="m-0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-lg-flex flex-lg-row-fluid w-50 bgi-size-cover bgi-position-y-center bgi-position-x-start bgi-no-repeat" style="background-image: url(/metronic8/demo38/assets/media/auth/bg11.png)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
135
Moonlight/Shared/Views/Account/Index.razor
Normal file
135
Moonlight/Shared/Views/Account/Index.razor
Normal file
@@ -0,0 +1,135 @@
|
||||
@page "/account"
|
||||
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Models.Forms
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
@inject ToastService ToastService
|
||||
|
||||
<AccountNavigation Index="0"/>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-between align-items-center">
|
||||
<div class="col">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="symbol symbol-circle symbol-35px symbol-md-45px">
|
||||
@if (IdentityService.CurrentUser.Avatar == null)
|
||||
{
|
||||
<img src="/assets/img/avatar.png" alt="Avatar">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/api/bucket/avatars/@(IdentityService.CurrentUser.Avatar)" alt="Avatar">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col ms-n2">
|
||||
<h4 class="mb-1">
|
||||
Your avatar
|
||||
</h4>
|
||||
<small class="text-body-secondary">
|
||||
PNG or JPG no bigger than 1000px wide and tall.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-center">
|
||||
<SmartFileSelect @ref="SmartFileSelect"/>
|
||||
<WButton OnClick="OnUpload" Text="Upload" CssClasses="ms-2 btn btn-primary" WorkingText="Uploading"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<SmartForm Model="Form" OnValidSubmit="OnSubmit">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<div class="mb-5">
|
||||
<label class="form-label">Username</label>
|
||||
<input @bind="Form.Username" class="form-control form-control-solid" type="text" />
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label class="form-label">Email</label>
|
||||
<input @bind="Form.Email" class="form-control form-control-solid" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
|
||||
<div class="card card-body mt-5">
|
||||
<div class="text-end">
|
||||
<ConfirmButton OnClick="OnDelete" Text="Delete account" CssClasses="btn btn-danger" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@code
|
||||
{
|
||||
private UpdateAccountForm Form = new();
|
||||
private SmartFileSelect SmartFileSelect;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Form.Username = IdentityService.CurrentUser.Username;
|
||||
Form.Email = IdentityService.CurrentUser.Email;
|
||||
}
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
await UserService.Update(
|
||||
IdentityService.CurrentUser,
|
||||
Form.Username,
|
||||
Form.Email
|
||||
);
|
||||
|
||||
await ToastService.Success("Successfully saved changes");
|
||||
}
|
||||
|
||||
private async Task OnUpload()
|
||||
{
|
||||
// Validation
|
||||
if (SmartFileSelect.SelectedFile == null)
|
||||
return;
|
||||
|
||||
if (!Formatter.EndsInOneOf(SmartFileSelect.SelectedFile.Name, new[]
|
||||
{
|
||||
".jpg",
|
||||
".png"
|
||||
}))
|
||||
{
|
||||
await ToastService.Danger("You are only allowed to upload JPG and PNG files");
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we are actually working
|
||||
await UserService.Details.UpdateAvatar(
|
||||
IdentityService.CurrentUser,
|
||||
SmartFileSelect.SelectedFile.OpenReadStream(SmartFileSelect.MaxFileSize),
|
||||
SmartFileSelect.SelectedFile.Name
|
||||
);
|
||||
|
||||
// Reset
|
||||
await SmartFileSelect.RemoveSelection();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await ToastService.Success("Successfully uploaded avatar");
|
||||
}
|
||||
|
||||
private async Task OnDelete()
|
||||
{
|
||||
await UserService.Delete(IdentityService.CurrentUser);
|
||||
await IdentityService.Authenticate();
|
||||
}
|
||||
}
|
||||
158
Moonlight/Shared/Views/Account/Security.razor
Normal file
158
Moonlight/Shared/Views/Account/Security.razor
Normal file
@@ -0,0 +1,158 @@
|
||||
@page "/account/security"
|
||||
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Users
|
||||
@using OtpNet
|
||||
@using QRCoder
|
||||
@using Moonlight.App.Models.Enums
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
@inject ToastService ToastService
|
||||
|
||||
<AccountNavigation Index="1"/>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card">
|
||||
<SmartForm Model="PasswordForm" OnValidSubmit="OnPasswordSubmit">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div>
|
||||
<label class="form-label">New password</label>
|
||||
<input @bind="PasswordForm.Password" type="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div>
|
||||
<label class="form-label">New password repeated</label>
|
||||
<input @bind="PasswordForm.RepeatedPassword" type="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">Update password</button>
|
||||
</div>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
@if (IdentityService.Flags[UserFlag.TotpEnabled])
|
||||
{
|
||||
<div class="card card-body fs-6">
|
||||
<span class="card-title text-success">Your account is secured with 2fa</span>
|
||||
<div class="text-center">
|
||||
<ConfirmButton OnClick="OnDisable2FA" Text="Disable 2fa" CssClasses="btn btn-danger" WorkingText="Disabling"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (!IdentityService.Flags[UserFlag.TotpEnabled] && IdentityService.CurrentUser.TotpKey != null)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body fs-6">
|
||||
<p>
|
||||
Scan the qr code and enter the code generated by the app you have scanned it in
|
||||
</p>
|
||||
@{
|
||||
QRCodeGenerator qrGenerator = new QRCodeGenerator();
|
||||
|
||||
var qrCodeData = qrGenerator.CreateQrCode
|
||||
(
|
||||
$"otpauth://totp/{Uri.EscapeDataString(IdentityService.CurrentUser.Email)}?secret={IdentityService.CurrentUser.TotpKey}&issuer={Uri.EscapeDataString("Moonlight")}",
|
||||
QRCodeGenerator.ECCLevel.Q
|
||||
);
|
||||
|
||||
PngByteQRCode qrCode = new PngByteQRCode(qrCodeData);
|
||||
byte[] qrCodeAsPngByteArr = qrCode.GetGraphic(5);
|
||||
var base64 = Convert.ToBase64String(qrCodeAsPngByteArr);
|
||||
}
|
||||
<div class="text-center">
|
||||
<img src="data:image/png;base64,@(base64)" alt="QR Code" class="img-fluid">
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<span class="h3">@(IdentityService.CurrentUser.TotpKey)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<SmartForm Model="CodeForm" OnValidSubmit="On2FASubmit">
|
||||
<div class="input-group">
|
||||
<input @bind="CodeForm.Code" type="number" class="form-control"/>
|
||||
<button type="submit" class="btn btn-primary">Enable 2fa</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card card-body fs-6">
|
||||
<span class="card-title">Secure your account using 2fa</span>
|
||||
<p>
|
||||
Make sure you have installed one of the following apps on your smartphone and continue
|
||||
</p>
|
||||
|
||||
<a href="https://support.google.com/accounts/answer/1066447?hl=en" target="_blank">Google Authenticator</a>
|
||||
<br/>
|
||||
<a href="https://www.microsoft.com/en-us/account/authenticator" target="_blank">Microsoft Authenticator</a>
|
||||
<br/>
|
||||
<a href="https://authy.com/download/" target="_blank">Authy</a>
|
||||
<br/>
|
||||
<a href="https://support.1password.com/one-time-passwords/" target="_blank">1Password</a>
|
||||
<br/>
|
||||
<div class="text-center">
|
||||
<WButton OnClick="OnSeed2FA" Text="Enable 2fa" CssClasses="btn btn-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private UpdateAccountPasswordForm PasswordForm = new();
|
||||
private TwoFactorCodeForm CodeForm = new();
|
||||
|
||||
private async Task OnPasswordSubmit()
|
||||
{
|
||||
if (PasswordForm.Password != PasswordForm.RepeatedPassword)
|
||||
throw new DisplayException("The passwords do not match");
|
||||
|
||||
await UserService.Auth.ChangePassword(IdentityService.CurrentUser, PasswordForm.Password);
|
||||
await ToastService.Success("Successfully updated your password");
|
||||
await IdentityService.Authenticate();
|
||||
}
|
||||
|
||||
private async Task OnDisable2FA()
|
||||
{
|
||||
await UserService.Auth.SetTotp(IdentityService.CurrentUser, false);
|
||||
await IdentityService.Authenticate();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnSeed2FA()
|
||||
{
|
||||
await UserService.Auth.SeedTotp(IdentityService.CurrentUser);
|
||||
await IdentityService.Authenticate();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task On2FASubmit()
|
||||
{
|
||||
var totp = new Totp(Base32Encoding.ToBytes(IdentityService.CurrentUser.TotpKey));
|
||||
var code = totp.ComputeTotp();
|
||||
|
||||
if (code != CodeForm.Code)
|
||||
throw new DisplayException("The 2fa code you entered is invalid");
|
||||
|
||||
CodeForm = new();
|
||||
|
||||
await UserService.Auth.SetTotp(IdentityService.CurrentUser, true);
|
||||
await IdentityService.Authenticate();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user