Implemented basic user auth, register, login, details and avatar stuff from helio

This commit is contained in:
Marcel Baumgartner
2023-10-15 19:19:47 +02:00
parent 3bb4e7daab
commit 49c893f515
41 changed files with 2079 additions and 212 deletions

View 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 &amp; 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("/");
}
}

View 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 &amp; 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("/");
}
}

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

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
@using System.Diagnostics
@using Moonlight.App.Exceptions
@inherits ErrorBoundaryBase
@if (Crashed)

View File

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

View File

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

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

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

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