Add admin project, add separate admin and user identity tables (#113)
@@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.SmtpService", "s
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.IntegrationTests", "src\Tests\AliasVault.IntegrationTests\AliasVault.IntegrationTests.csproj", "{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Admin", "src\AliasVault.Admin\AliasVault.Admin.csproj", "{7D21383B-7B47-4CE6-92D9-BB2A99AE3E50}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -103,6 +105,10 @@ Global
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7D21383B-7B47-4CE6-92D9-BB2A99AE3E50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7D21383B-7B47-4CE6-92D9-BB2A99AE3E50}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7D21383B-7B47-4CE6-92D9-BB2A99AE3E50}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7D21383B-7B47-4CE6-92D9-BB2A99AE3E50}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
BIN
database/AliasServerDb.sqlite-bak-before-admin
Normal file
25
src/AliasVault.Admin/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
50
src/AliasVault.Admin/AliasVault.Admin.csproj
Normal file
@@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\net8.0\AliasVault.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\Release\net8.0\AliasVault.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AliasGenerators\AliasGenerators.csproj" />
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Utilities\FaviconExtractor\FaviconExtractor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.md">
|
||||
<Link>LICENSE.md</Link>
|
||||
</Content>
|
||||
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Components\Pages\Aliases\AddEdit.razor" />
|
||||
<_ContentIncludedByDefault Remove="Components\Pages\Aliases\Alias.razor" />
|
||||
<_ContentIncludedByDefault Remove="Components\Pages\Aliases\Delete.razor" />
|
||||
<_ContentIncludedByDefault Remove="Components\Pages\Aliases\View.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
85
src/AliasVault.Admin/Areas/User/Pages/EmailConfirm.cshtml
Normal file
@@ -0,0 +1,85 @@
|
||||
@page "/user/email-confirm"
|
||||
@using AliasServerDb
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
|
||||
@if (Success)
|
||||
{
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Email address confirmed!</h2>
|
||||
|
||||
<p class="mb-5">
|
||||
Your email address has been successfully confirmed. You can now log in to your account.
|
||||
</p>
|
||||
|
||||
<a href="/" class="btn btn-primary"><span class="material-symbols-rounded align-middle me-2">arrow_back</span>Log in</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Confirm email address</h2>
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@Error
|
||||
</div>
|
||||
|
||||
<a href="/" class="btn btn-primary"><span class="material-symbols-rounded align-middle me-2">arrow_back</span>Return to login</a>
|
||||
}
|
||||
|
||||
@functions {
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = "";
|
||||
public string Error { get; set; } = "";
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var userId = Request.Query["userId"];
|
||||
var code = Request.Query["code"];
|
||||
|
||||
var user = await UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
// Error: User not found
|
||||
Error = "Invalid URL";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var result = await UserManager.ConfirmEmailAsync(user, code);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
// Email confirmed!
|
||||
Success = true;
|
||||
// Check if user is logged in already, if so, redirect to homepage
|
||||
if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
return Redirect("~/?successMessage=EmailConfirmed");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show success message to tell user they can now log in
|
||||
return Redirect("/user/login?emailConfirmed=true");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Error: Invalid token
|
||||
// Convert errors to ErrorString
|
||||
Error = "Custom errors: \n";
|
||||
result.Errors.ToList().ForEach(e => Error += e.Description + "\n");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
29
src/AliasVault.Admin/Areas/User/Pages/ForgotPassword.cshtml
Normal file
@@ -0,0 +1,29 @@
|
||||
@page "/user/forgot-password"
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@model ForgotPasswordModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Forgot password";
|
||||
}
|
||||
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Forgot password</h2>
|
||||
<p class="text-white-50 mb-4">Enter your email to get a password reset link.</p>
|
||||
|
||||
<form id="account" method="post" class=" z-index-1 position-relative needs-validation" novalidate="">
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<input asp-for="Input.Email" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<p class="small mb-5 pb-lg-2"><a class="text-white-50" href="/user/login">Remember your password? Sign in.</a></p>
|
||||
|
||||
<button data-mdb-button-init data-mdb-ripple-init class="btn btn-outline-light btn-lg px-5" type="submit">Send password reset email</button>
|
||||
|
||||
<div class="d-flex justify-content-center text-center mt-4 pt-1">
|
||||
<a href="#!" class="text-white"><i class="fab fa-facebook-f fa-lg"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-twitter fa-lg mx-4 px-2"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-google fa-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Web;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
//using SendgridEmail;
|
||||
|
||||
namespace AliasVault.Areas.User.Pages;
|
||||
|
||||
public class ForgotPasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<AdminUser> _userManager;
|
||||
private readonly IConfiguration _configuration;
|
||||
//private readonly EmailService _emailService;
|
||||
|
||||
public ForgotPasswordModel(SignInManager<AdminUser> signInManager, UserManager<AdminUser> userManager, IConfiguration configuration
|
||||
//EmailService emailService
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_configuration = configuration;
|
||||
//_emailService = emailService;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = new();
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// User with the given email not found.
|
||||
// For security reasons, don't reveal this information.
|
||||
return LocalRedirect("/user/login?passwordReset=true");
|
||||
}
|
||||
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
string encodedToken = HttpUtility.UrlEncode(token);
|
||||
|
||||
// Send the token to the user's email
|
||||
string scheme = HttpContext.Request.Scheme; // "http" or "https"
|
||||
string host = HttpContext.Request.Host.Value; // the host name and port
|
||||
string baseUrl = $"{scheme}://{host}/";
|
||||
//await _emailService.SendUserPasswordForgotMailAsync(user, "UserEmailConfirm", baseUrl, encodedToken, _configuration["SendGridApiKey"] ?? "");
|
||||
|
||||
return LocalRedirect("/user/login?passwordReset=true");
|
||||
|
||||
// Add the errors from the result to the model state
|
||||
/*foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}*/
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
49
src/AliasVault.Admin/Areas/User/Pages/Login.cshtml
Normal file
@@ -0,0 +1,49 @@
|
||||
@page "/user/login"
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@model LoginModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Login</h2>
|
||||
<p class="text-white-50 mb-4">Please enter your login and password.</p>
|
||||
|
||||
<form id="account" method="post" class=" z-index-1 position-relative needs-validation" novalidate="">
|
||||
|
||||
@if (!Model.SuccessMessage.IsNullOrEmpty())
|
||||
{
|
||||
<div class="alert alert-success" role="alert">
|
||||
@Model.SuccessMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<input asp-for="Input.Email" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<input asp-for="Input.Password" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<p class="small mb-5 pb-lg-2"><a class="text-white-50" href="/user/forgot-password">Forgot password?</a></p>
|
||||
|
||||
<button data-mdb-button-init data-mdb-ripple-init class="btn btn-outline-light btn-lg px-5" type="submit">Login</button>
|
||||
|
||||
<div class="d-flex justify-content-center text-center mt-4 pt-1">
|
||||
<a href="#!" class="text-white"><i class="fab fa-facebook-f fa-lg"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-twitter fa-lg mx-4 px-2"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-google fa-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<p class="mb-0">Don't have an account? <a href="/user/register" class="text-white-50 fw-bold">Sign Up</a>
|
||||
</p>
|
||||
</div>
|
||||
115
src/AliasVault.Admin/Areas/User/Pages/Login.cshtml.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using AliasVault.Admin.Services;
|
||||
|
||||
namespace AliasVault.Areas.User.Pages;
|
||||
|
||||
public class LoginModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<AdminUser> _signInManager;
|
||||
private readonly UserService _userService;
|
||||
|
||||
public LoginModel(SignInManager<AdminUser> signInManager, UserService userService)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = new();
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string SuccessMessage { get; set; } = "";
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
if (!Request.Query["returnUrl"].IsNullOrEmpty())
|
||||
{
|
||||
ReturnUrl = Request.Query["returnUrl"].ToString();
|
||||
}
|
||||
|
||||
if (!Request.Query["emailConfirmed"].IsNullOrEmpty())
|
||||
{
|
||||
if (Request.Query["emailConfirmed"] == "true")
|
||||
{
|
||||
SuccessMessage = "Your email address has been confirmed. You can now log in.";
|
||||
}
|
||||
}
|
||||
else if (!Request.Query["passwordReset"].IsNullOrEmpty())
|
||||
{
|
||||
if (Request.Query["passwordReset"] == "true")
|
||||
{
|
||||
SuccessMessage = "Check your mailbox for the password reset link.";
|
||||
}
|
||||
}
|
||||
else if (!Request.Query["passwordChanged"].IsNullOrEmpty())
|
||||
{
|
||||
if (Request.Query["passwordChanged"] == "true")
|
||||
{
|
||||
SuccessMessage = "Your password has been changed. You can now log in.";
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user is already logged in
|
||||
if (_signInManager.IsSignedIn(User))
|
||||
{
|
||||
// Redirect to home page
|
||||
Response.Redirect(GetInAppRedirectUrl());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var result = await _signInManager.PasswordSignInAsync(Input.Email ?? "", Input.Password ?? "", isPersistent: false,
|
||||
lockoutOnFailure: false);
|
||||
|
||||
if (Request.Query["returnUrl"] != String.Empty)
|
||||
{
|
||||
ReturnUrl = Request.Query["returnUrl"].ToString();
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return LocalRedirect(GetInAppRedirectUrl());
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
// The account hasn't been confirmed
|
||||
ModelState.AddModelError(string.Empty, "You must confirm your email address before you can sign in.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add error to the model state
|
||||
ModelState.AddModelError(string.Empty, "The login attempt failed. Check if the email and password are correct and try again. If you don't remember your password, you can reset it.");
|
||||
}
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public string GetInAppRedirectUrl()
|
||||
{
|
||||
if (ReturnUrl != null && Url.IsLocalUrl(ReturnUrl))
|
||||
{
|
||||
return ReturnUrl;
|
||||
}
|
||||
|
||||
return "/";
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
}
|
||||
27
src/AliasVault.Admin/Areas/User/Pages/Logout.cshtml
Normal file
@@ -0,0 +1,27 @@
|
||||
@page "/user/logout"
|
||||
@using AliasServerDb
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
|
||||
@functions {
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
return Redirect("~/");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
55
src/AliasVault.Admin/Areas/User/Pages/Register.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@page "/user/register"
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@inject IConfiguration Configuration
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model AliasVault.Areas.User.Pages.RegisterModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Create account</h2>
|
||||
<p class="text-white-50 mb-4">To get started, please register your account.</p>
|
||||
|
||||
<form id="account" method="post" class=" z-index-1 position-relative needs-validation" novalidate="">
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<input asp-for="Input.Email" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<input asp-for="Input.Password" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.PasswordConfirm" class="form-label"></label>
|
||||
<input asp-for="Input.PasswordConfirm" class="form-control"></input>
|
||||
<span asp-validation-for="Input.PasswordConfirm" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<div>
|
||||
<input asp-for="Input.AcceptTerms" class="form-check-input"/>
|
||||
<label for="Input_AcceptTerms" class="form-label">I agree with the <a href="@(Configuration["WebshopBaseUrl"])/terms-conditions" target="_blank">terms and conditions</a></label>
|
||||
</div>
|
||||
<span asp-validation-for="Input.AcceptTerms" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button data-mdb-button-init data-mdb-ripple-init class="btn btn-outline-light btn-lg px-5" type="submit">Register</button>
|
||||
|
||||
<div class="d-flex justify-content-center text-center mt-4 pt-1">
|
||||
<a href="#!" class="text-white"><i class="fab fa-facebook-f fa-lg"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-twitter fa-lg mx-4 px-2"></i></a>
|
||||
<a href="#!" class="text-white"><i class="fab fa-google fa-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<p class="mb-0">Already have an account? <a href="/user/login" class="text-white-50 fw-bold">Sign In</a>
|
||||
</p>
|
||||
</div>
|
||||
78
src/AliasVault.Admin/Areas/User/Pages/Register.cshtml.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace AliasVault.Areas.User.Pages;
|
||||
|
||||
public class RegisterModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<AdminUser> _signInManager;
|
||||
private readonly UserManager<AdminUser> _userManager;
|
||||
|
||||
public RegisterModel(SignInManager<AdminUser> signInManager, UserManager<AdminUser> userManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[BindProperty] public InputModel Input { get; set; } = new();
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
// Check if the user is already logged in
|
||||
if (_signInManager.IsSignedIn(User))
|
||||
{
|
||||
// Redirect to user login page (which will redirect to home page)
|
||||
Response.Redirect("/user/login");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var identity = new AdminUser { UserName = Input.Email, Email = Input.Email };
|
||||
var result = await _userManager.CreateAsync(identity, Input.Password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _signInManager.SignInAsync(identity, isPersistent: false);
|
||||
return LocalRedirect("/");
|
||||
}
|
||||
|
||||
// Add the errors from the result to the model state
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required] [EmailAddress] public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
|
||||
//[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$", ErrorMessage = "Password must contain at least one uppercase letter, one lowercase letter, and one number.")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[DisplayName("Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "Password and confirmation password do not match.")]
|
||||
public string PasswordConfirm { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[DisplayName("I agree with the terms and conditions")]
|
||||
[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms and conditions.")]
|
||||
public bool AcceptTerms { get; set; }
|
||||
}
|
||||
}
|
||||
32
src/AliasVault.Admin/Areas/User/Pages/ResetPassword.cshtml
Normal file
@@ -0,0 +1,32 @@
|
||||
@page "/user/reset-password"
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@model ResetPasswordModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Reset password";
|
||||
}
|
||||
|
||||
<partial name="_Logo" />
|
||||
<h2 class="fw-bold mb-2 text-uppercase">Enter new password</h2>
|
||||
<p class="text-white-50 mb-4">Enter a new password for your account.</p>
|
||||
|
||||
<form id="account" method="post" class=" z-index-1 position-relative needs-validation" novalidate="">
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<input asp-for="Input.Password" class="form-control"></input>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div data-mdb-input-init class="form-outline form-white mb-4">
|
||||
<label asp-for="Input.PasswordConfirm" class="form-label"></label>
|
||||
<input asp-for="Input.PasswordConfirm" class="form-control"></input>
|
||||
<span asp-validation-for="Input.PasswordConfirm" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button data-mdb-button-init data-mdb-ripple-init class="btn btn-outline-light btn-lg px-5" type="submit">Change Password</button>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<p class="mb-0">Remember your password? <a href="/user/login" class="text-white-50 fw-bold">Sign In</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace AliasVault.Areas.User.Pages;
|
||||
|
||||
public class ResetPasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<AdminUser> _userManager;
|
||||
|
||||
public ResetPasswordModel(UserManager<AdminUser> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = new();
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var userId = Request.Query["userId"];
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
// Error: User not found
|
||||
return LocalRedirect("/user/login");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var userId = Request.Query["userId"];
|
||||
var code = Request.Query["code"];
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
// Error: User not found
|
||||
return LocalRedirect("/user/login");
|
||||
}
|
||||
|
||||
var result = await _userManager.ResetPasswordAsync(user, code, Input.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return LocalRedirect("/user/login?passwordChanged=true");
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
[DisplayName("Enter new password")]
|
||||
[DataType(DataType.Password)]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
//[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$", ErrorMessage = "Password must contain at least one uppercase letter, one lowercase letter, and one number.")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[DisplayName("Confirm new password")]
|
||||
[Compare("Password", ErrorMessage = "Password and confirmation password do not match.")]
|
||||
public string PasswordConfirm { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<img src="horizontal-logo-cropped.png" alt="AliasesVault" class="img-fluid" style="max-width: 330px;"/>
|
||||
<hr />
|
||||
62
src/AliasVault.Admin/Areas/User/_IdentityLayout.cshtml
Normal file
@@ -0,0 +1,62 @@
|
||||
@namespace AliasVault.Areas.User
|
||||
@using AliasVault.Admin.Services;
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject VersionedContentService VersionService
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="~/"/>
|
||||
<link rel="stylesheet" href="@VersionService.GetVersionedPath("bootstrap/bootstrap.min.css")"/>
|
||||
<link href="@VersionService.GetVersionedPath("app.css")" rel="stylesheet"/>
|
||||
<link href="AliasVault.styles.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<!--Google web fonts-->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital@0;1&family=Open+Sans:wght@300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0"/>
|
||||
<title>AliasesVault</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--////////////////// PreLoader Start//////////////////////-->
|
||||
<div class="loader bg-gradient-primary text-white" style="display: none;">
|
||||
<!--Placeholder animated layout for preloader-->
|
||||
<div class="d-flex flex-column flex-root">
|
||||
<div class="page d-flex flex-row flex-column-fluid">
|
||||
<div class="page-content ps-0 ms-0 d-flex flex-column flex-row-fluid">
|
||||
<div class="content flex-column p-4 pb-0 d-flex justify-content-center align-items-center flex-column-fluid position-relative">
|
||||
<div class="w-100 h-100 position-relative d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader anim-spinner me-2">
|
||||
<line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
|
||||
</svg>
|
||||
<div>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--////////////////// /.PreLoader END//////////////////////-->
|
||||
|
||||
<section class="vh-100 gradient-custom">
|
||||
<div class="container py-5 h-100">
|
||||
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card bg-dark text-white" style="border-radius: 1rem;">
|
||||
<div class="card-body p-5 text-center">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
7
src/AliasVault.Admin/Areas/User/_ViewImports.cshtml
Normal file
@@ -0,0 +1,7 @@
|
||||
@using AliasVault
|
||||
@using AliasVault.Areas.User
|
||||
@using AliasVault.Areas.User.Pages
|
||||
@using AliasVault.Areas.User.Pages.Shared
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@namespace AliasVault.Areas.User
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
src/AliasVault.Admin/Areas/User/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_IdentityLayout.cshtml";
|
||||
}
|
||||
15
src/AliasVault.Admin/Components/App.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@using AliasVault.Admin.Components.Layout
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<PageNotFound />
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
@@ -0,0 +1,37 @@
|
||||
using AliasVault.Admin.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AliasVault.Components.Layouts.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the current event has the Faqs plugin enabled. If not, user is redirected
|
||||
/// to the plugin disabled page.
|
||||
/// </summary>
|
||||
public class BaseInAppLayout : LayoutComponentBase
|
||||
{
|
||||
[Inject]
|
||||
public JsInvokeService JsInvokeService { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
public NavigationManager NavigationManager { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
public UserService UserService { get; set; } = null!;
|
||||
|
||||
protected bool AccessCheckCompleted;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Call the GenericAccessCheckAsync method and halt execution if a redirect is required.
|
||||
AccessCheckCompleted = await AccessCheckService.GenericAccessCheckAsync(NavigationManager, UserService);
|
||||
StateHasChanged();
|
||||
|
||||
// Active main theme init logic (darkmode, menu toggle)
|
||||
await JsInvokeService.RetryInvokeAsync("mainThemeInit", TimeSpan.FromMilliseconds(200), 5);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
src/AliasVault.Admin/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,24 @@
|
||||
@inherits AliasVault.Components.Layouts.Base.BaseInAppLayout
|
||||
|
||||
@if (!AccessCheckCompleted)
|
||||
{
|
||||
<div class="mt-10">
|
||||
<FullPageLoadingAnimation />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<TopMenu />
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- Content -->
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
@Body
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
|
||||
|
||||
|
||||
<div class="content-backdrop fade"></div>
|
||||
</div>
|
||||
}
|
||||
96
src/AliasVault.Admin/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
29
src/AliasVault.Admin/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">AliasVault</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
105
src/AliasVault.Admin/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
104
src/AliasVault.Admin/Components/Layout/TopMenu.razor
Normal file
@@ -0,0 +1,104 @@
|
||||
@using AliasVault.Admin.Services
|
||||
@inject UserService UserService
|
||||
|
||||
<nav class="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme" id="layout-navbar">
|
||||
<a href="/"><img src="horizontal-logo-cropped.png" alt="Logo" class="d-block d-xl-none" style="width: 170px;"></a>
|
||||
|
||||
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
||||
<!-- Search -->
|
||||
<div class="navbar-nav align-items-center">
|
||||
<div class="nav-item d-flex align-items-center">
|
||||
<i class="bx bx-search fs-4 lh-0"></i>
|
||||
<input type="text" class="form-control mt-3 border-0 shadow-none" placeholder="Search..." aria-label="Search...">
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Search -->
|
||||
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
<!-- Place this tag where you want the button to render. -->
|
||||
<li class="nav-item lh-1 me-3">
|
||||
<span></span>
|
||||
</li>
|
||||
|
||||
<!-- User -->
|
||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||
<a class="nav-link dropdown-toggle hide-arrow" href="javascript:void(0);" data-bs-toggle="dropdown">
|
||||
<div class="avatar avatar-online">
|
||||
<img src="../assets/img/avatars/1.webp" alt="" class="w-px-40 h-auto rounded-circle">
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<div class="d-flex">
|
||||
<div class="flex-shrink-0 me-3">
|
||||
<div class="avatar avatar-online">
|
||||
<img src="../assets/img/avatars/1.webp" alt="" class="w-px-40 h-auto rounded-circle">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<span class="fw-semibold d-block">@_username</span>
|
||||
<small class="text-muted">User</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-divider"></div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="bx bx-user me-2"></i>
|
||||
<span class="align-middle">My Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="bx bx-cog me-2"></i>
|
||||
<span class="align-middle">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<span class="d-flex align-items-center align-middle">
|
||||
<i class="flex-shrink-0 bx bx-credit-card me-2"></i>
|
||||
<span class="flex-grow-1 align-middle">Billing</span>
|
||||
<span class="flex-shrink-0 badge badge-center rounded-pill bg-danger w-px-20 h-px-20">4</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-divider"></div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/user/logout">
|
||||
<i class="bx bx-power-off me-2"></i>
|
||||
<span class="align-middle">Log Out</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!--/ User -->
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
private string _username = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (UserService.UserLoaded)
|
||||
{
|
||||
_username = UserService.User().Email ?? "";
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
108
src/AliasVault.Admin/Components/Pages/AuthorizePageBase.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using AliasVault.Admin.Models;
|
||||
using AliasVault.Admin.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AliasVault.Admin.Components.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Base authorize page that all pages that are part of the logged in website should inherit from.
|
||||
/// All pages that inherit from this class will require the user to be logged in and have a confirmed email.
|
||||
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
public class AuthorizePageBase : OwningComponentBase
|
||||
{
|
||||
[Inject]
|
||||
public NavigationManager NavigationManager { get; set; } = null!;
|
||||
[Inject]
|
||||
protected UserService UserService { get; set; } = null!;
|
||||
[Inject]
|
||||
protected PortalMessageService PortalMessageService { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
public JsInvokeService JsInvokeService { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime Js { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
protected List<BreadcrumbItem> BreadcrumbItems { get; set; } = new List<BreadcrumbItem>();
|
||||
|
||||
private bool _parametersInitialSet;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AccessCheck()) return;
|
||||
await base.OnInitializedAsync();
|
||||
_parametersInitialSet = false;
|
||||
|
||||
// Add base breadcrumbs
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationManager.BaseUri });
|
||||
|
||||
// Detect success messages in query string and add them to the SuccessMessages list
|
||||
var uri = new Uri(NavigationManager.Uri);
|
||||
PortalMessageService.RetrieveMessagesFromQueryString(uri);
|
||||
|
||||
// Call the GenericAccessCheckAsync method and halt execution if a redirect is required.
|
||||
var verifyAccessCheckTask = await AccessCheckService.GenericAccessCheckAsync(NavigationManager, UserService);
|
||||
if (verifyAccessCheckTask != true)
|
||||
{
|
||||
// Keep the page from loading if the user is not authorized by calling an infinite loop.
|
||||
// We wait for navigation to happen during the infinite loop.
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issue a redirect to the login page if the user is not logged in.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected async Task<bool> AccessCheck()
|
||||
{
|
||||
// Call the GenericAccessCheckAsync method and halt execution if a redirect is required.
|
||||
var verifyAccessCheckTask = await AccessCheckService.GenericAccessCheckAsync(NavigationManager, UserService);
|
||||
if (verifyAccessCheckTask != true)
|
||||
{
|
||||
// Keep the page from loading if the user is not authorized by calling an infinite loop.
|
||||
// We wait for navigation to happen during the infinite loop.
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
// This is needed to prevent the OnParametersSetAsync method from running together with OnInitialized on initial page load.
|
||||
if (!_parametersInitialSet)
|
||||
{
|
||||
_parametersInitialSet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the GenericAccessCheckAsync method and halt execution if a redirect is required.
|
||||
var verifyAccessCheckTask = await AccessCheckService.GenericAccessCheckAsync(NavigationManager, UserService);
|
||||
if (verifyAccessCheckTask != true)
|
||||
{
|
||||
// Keep the page from loading if the user is not authorized by calling an infinite loop.
|
||||
// We wait for navigation to happen during the infinite loop.
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/AliasVault.Admin/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,37 @@
|
||||
@inherits AuthorizePageBase
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter] private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
|
||||
}
|
||||
65
src/AliasVault.Admin/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,65 @@
|
||||
@using AliasServerDb
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@inherits AuthorizePageBase
|
||||
@inject AliasServerDbContext dbContext
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Emails</PageTitle>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h1>Emails</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/add-alias" class="btn btn-success">
|
||||
<span class="bi bi-file-earmark-plus" aria-hidden="true"></span> + Add new alias
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p>Find all received emails below.</p>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
||||
@foreach (var email in Emails)
|
||||
{
|
||||
<div>
|
||||
<div>@email.From</div>
|
||||
<div>@email.MessagePreview</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private List<Email> Emails { get; set; } = new List<Email>();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAliasesAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Load the aliases from the database.
|
||||
var user = UserService.User();
|
||||
Emails = await dbContext.Emails
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
6
src/AliasVault.Admin/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,20 @@
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@inherits ComponentBase
|
||||
|
||||
@if (Message.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
|
||||
<span class="material-symbols-rounded align-middle me-2">error</span>
|
||||
<div>
|
||||
@Message
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@inherits ComponentBase
|
||||
|
||||
@if (Message.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="alert alert-success d-flex align-items-center alert-dismissible fade show" role="alert">
|
||||
<span class="material-symbols-rounded align-middle me-2">check_circle</span>
|
||||
<div>
|
||||
@Message
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
23
src/AliasVault.Admin/Components/Shared/Breadcrumb.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
@using AliasVault.Admin.Models
|
||||
@inherits ComponentBase
|
||||
|
||||
@if (BreadcrumbItems.Any())
|
||||
{
|
||||
<h4 class="fw-bold py-3 mb-4">
|
||||
<span class="text-muted fw-light">
|
||||
@foreach (var item in BreadcrumbItems)
|
||||
{
|
||||
@if (item != BreadcrumbItems.Last())
|
||||
{
|
||||
<span><a href="@item.Url">@item.DisplayName</a></span><text> / </text>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
@BreadcrumbItems.Last().DisplayName
|
||||
</h4>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public List<BreadcrumbItem> BreadcrumbItems { get; set; } = new List<BreadcrumbItem>();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label for="inputEmail3" class="col-sm-2 col-form-label">@Label</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly="readonly" aria-readonly="true" class="form-control @(_copied ? "is-valid" : "")" value="@Value" onclick="@CopyToClipboard">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Label { get; set; }
|
||||
[Parameter]
|
||||
public string Value { get; set; }
|
||||
|
||||
private bool _copied = false;
|
||||
|
||||
public async Task CopyToClipboard()
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("window.clipboardCopy.copyText", Value);
|
||||
_copied = true;
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
// After 2 seconds, remove the success class again
|
||||
await Task.Delay(2000);
|
||||
_copied = false;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
24
src/AliasVault.Admin/Components/Shared/DisplayFavicon.razor
Normal file
@@ -0,0 +1,24 @@
|
||||
@if (faviconBytes != null)
|
||||
{
|
||||
<img src="@faviconDataUrl" style="width: 50px;" alt="Favicon" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="img/service-placeholder.webp" style="width: 50px;" alt="Favicon" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public byte[]? faviconBytes { get; set; }
|
||||
|
||||
private string? faviconDataUrl;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (faviconBytes != null)
|
||||
{
|
||||
string base64String = Convert.ToBase64String(faviconBytes);
|
||||
faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="col-12">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
15
src/AliasVault.Admin/Components/Shared/PageTitleAppend.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@using AliasVault.Pages
|
||||
@using Microsoft.Extensions.Configuration
|
||||
|
||||
<PageTitle>@PageTitlePrefix AliasVault - @PageTitleSuffix</PageTitle>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string PageTitleSuffix { get; set; }
|
||||
|
||||
public string PageTitlePrefix { get; set; } = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
}
|
||||
}
|
||||
12
src/AliasVault.Admin/Components/_Imports.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using AliasVault.Admin
|
||||
@using AliasVault.Admin.Components
|
||||
@using AliasVault.Admin.Components.Shared
|
||||
33
src/AliasVault.Admin/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Use the official .NET 8 SDK image to build the app
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the solution file to the /src directory in the container
|
||||
COPY aliasvault.sln ./
|
||||
|
||||
# Copy the project file to the /src/AliasVault directory in the container
|
||||
COPY src/AliasVault/AliasVault.csproj ./AliasVault/
|
||||
|
||||
# Restore dependencies for the AliasVault project
|
||||
RUN dotnet restore "./AliasVault/AliasVault.csproj" --verbosity detailed
|
||||
|
||||
# Copy the rest of the application code to the /src directory in the container
|
||||
COPY src/. ./
|
||||
|
||||
# Publish the application to the /app/publish directory in the container
|
||||
WORKDIR /src/AliasVault
|
||||
RUN dotnet publish -c Release -o /app --verbosity detailed
|
||||
|
||||
# Use the official ASP.NET Core runtime image to run the app
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
WORKDIR /app/AliasVault
|
||||
|
||||
# Copy the published output from the build stage to the runtime stage
|
||||
COPY --from=build /app ./
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 8082
|
||||
ENV ASPNETCORE_URLS=http://+:8082
|
||||
|
||||
ENTRYPOINT ["dotnet", "AliasVault.dll"]
|
||||
|
||||
75
src/AliasVault.Admin/Identity/ClaimsTransformer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Security.Claims;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AliasVault.Admin.Identity;
|
||||
|
||||
public class ClaimsTransformer : IClaimsTransformation
|
||||
{
|
||||
private readonly IDbContextFactory<AliasServerDbContext> _dbContextFactory;
|
||||
|
||||
public ClaimsTransformer(IDbContextFactory<AliasServerDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
// Check if the user is authenticated
|
||||
if (!principal.Identity?.IsAuthenticated ?? false)
|
||||
{
|
||||
return principal;
|
||||
}
|
||||
|
||||
using (var dbContext = await _dbContextFactory.CreateDbContextAsync())
|
||||
{
|
||||
// Get the user
|
||||
var user = await dbContext.AdminUsers.FirstOrDefaultAsync(u => u.UserName == principal.Identity.Name);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return principal;
|
||||
}
|
||||
|
||||
// Get the user's roles and claims in a single database call
|
||||
var userRolesAndClaims = await (
|
||||
from userRole in dbContext.AdminUserRoles
|
||||
join role in dbContext.AdminRoles on userRole.RoleId equals role.Id
|
||||
where userRole.UserId == user.Id
|
||||
select new { userRole, role }
|
||||
).ToListAsync();
|
||||
|
||||
var userClaims = await dbContext.AdminUserClaims.Where(x => x.UserId == user.Id).ToListAsync();
|
||||
|
||||
// Convert roles to claims
|
||||
var roleClaims = userRolesAndClaims
|
||||
.Select(uc => new Claim(ClaimTypes.Role, uc.role.Name))
|
||||
.ToList();
|
||||
|
||||
// Add the user's claims to the role claims
|
||||
foreach (var userClaim in userClaims)
|
||||
{
|
||||
roleClaims.Add(new Claim(userClaim.ClaimType, userClaim.ClaimValue));
|
||||
}
|
||||
|
||||
// Filter out the role claims from the original claims, and use that to append to the new identity
|
||||
HashSet<string> roleTypesToRemove = new HashSet<string>
|
||||
{
|
||||
ClaimTypes.Role,
|
||||
//EventPermissionHandler.EventPermissionClaimType,
|
||||
};
|
||||
var nonRoleClaims = principal.Claims.Where(c => !roleTypesToRemove.Contains(c.Type));
|
||||
|
||||
// Create new identity and return it
|
||||
var identity = new ClaimsIdentity(
|
||||
nonRoleClaims.Concat(roleClaims),
|
||||
principal.Identity.AuthenticationType,
|
||||
ClaimTypes.Name,
|
||||
ClaimTypes.Role
|
||||
);
|
||||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AliasVault.Admin.Identity;
|
||||
|
||||
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
|
||||
: RevalidatingServerAuthenticationStateProvider where TUser : class
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IdentityOptions _options;
|
||||
|
||||
public RevalidatingIdentityAuthenticationStateProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_options = optionsAccessor.Value;
|
||||
}
|
||||
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||
var scope = _scopeFactory.CreateScope();
|
||||
try
|
||||
{
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
|
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scope is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (!userManager.SupportsUserSecurityStamp)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
|
||||
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||
return principalStamp == userStamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/AliasVault.Admin/Models/BreadcrumbItem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AliasVault.Admin.Models;
|
||||
|
||||
public class BreadcrumbItem
|
||||
{
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
154
src/AliasVault.Admin/Pages/_Host.cshtml
Normal file
@@ -0,0 +1,154 @@
|
||||
@page "/"
|
||||
@using AliasVault.Admin.Components
|
||||
@using AliasVault.Admin.Services
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@namespace AliasVault.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject VersionedContentService VersionService
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="~/"/>
|
||||
<link rel="stylesheet" href="@VersionService.GetVersionedPath("bootstrap/bootstrap.min.css")" />
|
||||
<link href="@VersionService.GetVersionedPath("app.css")" rel="stylesheet" />
|
||||
<link href="AliasVault.styles.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<!--Google web fonts-->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital@0;1&family=Open+Sans:wght@300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
|
||||
<!-- Core CSS -->
|
||||
<link rel="stylesheet" href="/assets/vendor/css/core.css" class="template-customizer-core-css" />
|
||||
<link rel="stylesheet" href="/assets/vendor/css/theme-default.css" class="template-customizer-theme-css" />
|
||||
<link rel="stylesheet" href="/assets/css/demo.css" />
|
||||
|
||||
<!-- Vendors CSS -->
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
<!-- Page CSS -->
|
||||
|
||||
<!-- Helpers -->
|
||||
<script src="/assets/vendor/js/helpers.js"></script>
|
||||
|
||||
<!--! Template customizer & Theme config files MUST be included after core stylesheets and helpers.js in the <head> section -->
|
||||
<!--? Config: Mandatory theme config file contain global vars & default theme options, Set your preferred theme option in this file. -->
|
||||
<script src="/assets/js/config.js"></script>
|
||||
|
||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
||||
</head>
|
||||
<body>
|
||||
<component type="typeof(App)" render-mode="Server"/>
|
||||
|
||||
<div id="components-reconnect-modal" class="aliasVault-reconnect-modal components-reconnect-hide">
|
||||
|
||||
<!-- Show class content -->
|
||||
<div class="show">
|
||||
<div class="position-fixed top-0 end-0 bottom-0 start-0 d-flex align-items-center justify-content-center" style="background-color: rgba(0, 0, 0, 0.5);">
|
||||
<div class="card text-center p-5 shadow" style="max-width: 32rem;">
|
||||
<p class="text-muted mb-4">
|
||||
The connection to AliasVault has been lost. Reload the page to continue.
|
||||
</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary">
|
||||
Reload the page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed class content -->
|
||||
<div class="failed">
|
||||
<div class="position-fixed top-0 end-0 bottom-0 start-0 d-flex align-items-center justify-content-center" style="background-color: rgba(0, 0, 0, 0.5);">
|
||||
<div class="card text-center p-5 shadow" style="max-width: 32rem;">
|
||||
<p class="text-muted mb-4">
|
||||
The connection to AliasVault has been lost. Reload the page to continue.
|
||||
</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary">
|
||||
Reload the page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rejected class content -->
|
||||
<div class="rejected">
|
||||
<div class="position-fixed top-0 end-0 bottom-0 start-0 d-flex align-items-center justify-content-center" style="background-color: rgba(0, 0, 0, 0.5);">
|
||||
<div class="card text-center p-5 shadow" style="max-width: 32rem;">
|
||||
<p class="text-muted mb-4">
|
||||
The connection to AliasVault has been lost. Reload the page to continue.
|
||||
</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary">
|
||||
Reload the page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
<environment include="Staging,Production">
|
||||
An error has occurred. This application may no longer respond until reloaded.
|
||||
</environment>
|
||||
<environment include="Development">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</environment>
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.clipboardCopy = {
|
||||
copyText: function (text) {
|
||||
navigator.clipboard.writeText(text).then(function () { })
|
||||
.catch(function (error) {
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.getFragment = () => {
|
||||
return window.location.hash;
|
||||
};
|
||||
|
||||
window.navigateToUrl = (url) => {
|
||||
try {
|
||||
window.location.href = url;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.isFunctionDefined = function(functionName) {
|
||||
return typeof window[functionName] === 'function';
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Core JS -->
|
||||
<!-- build:js assets/vendor/js/core.js -->
|
||||
<script src="/assets/vendor/libs/jquery/jquery.js"></script>
|
||||
<script src="/assets/vendor/libs/popper/popper.js"></script>
|
||||
<script src="/assets/vendor/js/bootstrap.js"></script>
|
||||
<script src="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.js"></script>
|
||||
|
||||
<script src="/assets/vendor/js/menu.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- Vendors JS -->
|
||||
|
||||
<!-- Main JS -->
|
||||
<script src="/assets/js/main.js"></script>
|
||||
|
||||
<!-- Page JS -->
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
100
src/AliasVault.Admin/Program.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Data.Common;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Admin;
|
||||
using AliasVault.Admin.Identity;
|
||||
using AliasVault.Admin.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// We use dbContextFactory to create a new instance of the DbContext for every place that needs it
|
||||
// as otherwise concurrency issues may occur if we use a single instance of the DbContext across the application.
|
||||
builder.Services.AddSingleton<DbConnection>(container =>
|
||||
{
|
||||
var configFile = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var connection = new SqliteConnection(configFile.GetConnectionString("AliasServerDbContext"));
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
});
|
||||
|
||||
builder.Services.AddDbContextFactory<AliasServerDbContext>((container, options) =>
|
||||
{
|
||||
var connection = container.GetRequiredService<DbConnection>();
|
||||
options.UseSqlite(connection).UseLazyLoadingProxies();
|
||||
});
|
||||
|
||||
builder.Services.AddDataProtection();
|
||||
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
options.TokenLifespan = TimeSpan.FromHours(12));
|
||||
|
||||
builder.Services.AddIdentity<AdminUser, AdminRole>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequiredUniqueChars = 0;
|
||||
options.SignIn.RequireConfirmedAccount = false;
|
||||
})
|
||||
.AddEntityFrameworkStores<AliasServerDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
builder.Services.AddScoped<JsInvokeService>();
|
||||
builder.Services.AddScoped<UserService>();
|
||||
builder.Services.AddScoped<PortalMessageService>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<AdminUser>>();
|
||||
builder.Services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
|
||||
builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot"));
|
||||
|
||||
// Force all app generated URLs to be lowercase as this improves SEO.
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
|
||||
// Add services to the container.
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
// Normal production use
|
||||
builder.Services.AddServerSideBlazor();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dev use
|
||||
builder.Services.AddServerSideBlazor()
|
||||
.AddCircuitOptions(e => {
|
||||
e.DetailedErrors = true;
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
await StartupTasks.CreateRolesIfNotExist(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
app.Run();
|
||||
38
src/AliasVault.Admin/Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:32869",
|
||||
"sslPort": 44372
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5280",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7004;http://localhost:5280",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/AliasVault.Admin/Services/AccessCheckService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AliasVault.Admin.Services;
|
||||
|
||||
public class AccessCheckService
|
||||
{
|
||||
public static async Task<bool> GenericAccessCheckAsync(NavigationManager navigationManager, UserService userService)
|
||||
{
|
||||
await userService.LoadCurrentUserAsync();
|
||||
|
||||
if (userService.UserLoaded)
|
||||
{
|
||||
// TODO: re-enable email confirmation check later.
|
||||
/*if (!userService.User().EmailConfirmed)
|
||||
{
|
||||
// Redirect to email confirmation page if we are not already there
|
||||
if (navigationManager.ToBaseRelativePath(navigationManager.Uri) != "account/confirm-email")
|
||||
{
|
||||
navigationManager.NavigateTo($"account/confirm-email");
|
||||
return false; // Halt further execution.
|
||||
}
|
||||
} */
|
||||
|
||||
// User is logged in and email is confirmed.
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
string returnUrl = navigationManager.ToBaseRelativePath(navigationManager.Uri);
|
||||
string loginUrl;
|
||||
|
||||
if (string.IsNullOrEmpty(returnUrl) || returnUrl == "/" || returnUrl == "user/login")
|
||||
{
|
||||
loginUrl = "user/login";
|
||||
}
|
||||
else
|
||||
{
|
||||
loginUrl = $"user/login?returnUrl=/{Uri.EscapeDataString(returnUrl)}";
|
||||
}
|
||||
|
||||
navigationManager.NavigateTo(loginUrl, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/AliasVault.Admin/Services/JSInvokeService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace AliasVault.Admin.Services;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
/// Service for invoking JavaScript functions from C#.
|
||||
/// </summary>
|
||||
public class JsInvokeService
|
||||
{
|
||||
private IJSRuntime Js { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsInvokeService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="js">The IJSRuntime object.</param>
|
||||
public JsInvokeService(IJSRuntime js)
|
||||
{
|
||||
Js = js;
|
||||
}
|
||||
|
||||
public async Task RetryInvokeAsync(string functionName, TimeSpan initialDelay, int maxAttempts, params object[] args)
|
||||
{
|
||||
TimeSpan delay = initialDelay;
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isDefined = await this.Js.InvokeAsync<bool>("isFunctionDefined", functionName);
|
||||
if (isDefined)
|
||||
{
|
||||
await this.Js.InvokeVoidAsync(functionName, args);
|
||||
return; // Successfully called the JS function, exit the method
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Optionally log the exception
|
||||
}
|
||||
|
||||
// Wait for the delay before the next attempt
|
||||
await Task.Delay(delay);
|
||||
|
||||
// Exponential backoff: double the delay for the next attempt
|
||||
delay = TimeSpan.FromTicks(delay.Ticks * 2);
|
||||
}
|
||||
|
||||
// Optionally log that the JS function could not be called after maxAttempts
|
||||
}
|
||||
}
|
||||
79
src/AliasVault.Admin/Services/PortalMessageService.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Web;
|
||||
|
||||
namespace AliasVault.Admin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Handles portal messages that should be displayed to the user, such as success or error messages. These messages
|
||||
/// are stored in this object which is scoped to the current session. This allows the messages to be cached until
|
||||
/// they actually have been displayed. So they can survive redirects and page reloads.
|
||||
/// </summary>
|
||||
public class PortalMessageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains success messages that should be displayed to the user. A default set of success messages is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
protected List<string> SuccessMessages { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Allow other components to subscribe to changes in the event object.
|
||||
/// </summary>
|
||||
public event Action? OnChange;
|
||||
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Public constructor which can be called from static async method or directly.
|
||||
/// </summary>
|
||||
public PortalMessageService()
|
||||
{
|
||||
}
|
||||
|
||||
public void AddSuccessMessage(string message, bool notifyStateChanged = true)
|
||||
{
|
||||
SuccessMessages.Add(message);
|
||||
|
||||
// Notify subscribers that a message has been added.
|
||||
if (notifyStateChanged)
|
||||
{
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary with messages that should be displayed to the user. After this method is called,
|
||||
/// the messages are automatically cleared.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, string> GetMessagesForDisplay()
|
||||
{
|
||||
var messages = new Dictionary<string, string>();
|
||||
foreach (var message in SuccessMessages)
|
||||
{
|
||||
messages.Add("success", message);
|
||||
}
|
||||
|
||||
// Clear messages
|
||||
SuccessMessages.Clear();
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves messages from the query string (if any) and adds them to the correct messages list.
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
public void RetrieveMessagesFromQueryString(Uri uri)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
var successMessage = query.Get("successMessage");
|
||||
if (!string.IsNullOrEmpty(successMessage))
|
||||
{
|
||||
switch (successMessage)
|
||||
{
|
||||
case "EmailConfirmed":
|
||||
AddSuccessMessage("Your email has successfully been confirmed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
388
src/AliasVault.Admin/Services/UserService.cs
Normal file
@@ -0,0 +1,388 @@
|
||||
namespace AliasVault.Admin.Services;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// User service for managing users.
|
||||
/// </summary>
|
||||
public class UserService
|
||||
{
|
||||
private readonly AliasServerDbContext _dbContext;
|
||||
private readonly UserManager<AdminUser> _userManager;
|
||||
private readonly SignInManager<AdminUser> _signInManager;
|
||||
private AdminUser? _user;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private const string AdminRole = "Admin";
|
||||
|
||||
/// <summary>
|
||||
/// The Event Ids that the current user is allowed to manage.
|
||||
/// </summary>
|
||||
private List<Guid> _managedEventIds = new();
|
||||
|
||||
/// <summary>
|
||||
/// The roles of the current user
|
||||
/// </summary>
|
||||
private IList<string> _userRoles = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current user is an admin or not.
|
||||
/// </summary>
|
||||
private bool _isAdmin;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if event is loaded and available, false if not. Use this before accessing Event() method.
|
||||
/// </summary>
|
||||
public bool UserLoaded => _user != null;
|
||||
|
||||
/// <summary>
|
||||
/// Allow other components to subscribe to changes in the event object.
|
||||
/// </summary>
|
||||
public event Action OnChange = delegate { };
|
||||
|
||||
private void NotifyStateChanged() => OnChange.Invoke();
|
||||
|
||||
public UserService(AliasServerDbContext dbContext, UserManager<AdminUser> userManager, SignInManager<AdminUser> signInManager, IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all users.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<AdminUser>> GetAllUsersAsync()
|
||||
{
|
||||
var userList = await _userManager.Users.ToListAsync();
|
||||
return userList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and returns user by id, using the _userManager instead of the _dbContext.
|
||||
/// This is necessary when performing actions on the user, such as changing password or deleting the object.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AdminUser> GetUserByIdUserManagerAsync(Guid userId)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||
if (user == null)
|
||||
{
|
||||
throw new Exception($"User with id {userId} not found.");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns inner event EF object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public AdminUser User()
|
||||
{
|
||||
if (_user == null)
|
||||
{
|
||||
throw new Exception("Trying to access User object which is null.");
|
||||
}
|
||||
|
||||
return _user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns managed Event ids list.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<Guid> UserAllowedEventIds()
|
||||
{
|
||||
return _managedEventIds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether current user is admin or not.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool CurrentUserIsAdmin()
|
||||
{
|
||||
return _isAdmin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current logged on user based on HttpContext.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task LoadCurrentUserAsync()
|
||||
{
|
||||
if (_httpContextAccessor.HttpContext != null)
|
||||
{
|
||||
|
||||
// Load user from database. Use a new context everytime to ensure we get the latest data.
|
||||
var user = await _dbContext.AdminUsers.FirstOrDefaultAsync(u => u.UserName == _httpContextAccessor.HttpContext.User.Identity.Name);
|
||||
if (user != null)
|
||||
{
|
||||
_user = user;
|
||||
|
||||
// Load managed event ids for current user.
|
||||
//_managedEventIds = await GetUserAllowedEventIdsAsync(_user);
|
||||
|
||||
// Load all roles for current user.
|
||||
_userRoles = await _userManager.GetRolesAsync(this.User());
|
||||
|
||||
// Define if current user is admin.
|
||||
_isAdmin = _userRoles.Contains(AdminRole);
|
||||
}
|
||||
|
||||
// UserManager implementation: throughout Blazor server session user is not updated when user is updated in database
|
||||
// because of UserManager EF cache. That's why we load it ourselves straight from the database via new DbContext
|
||||
// to ensure we get the latest data everytime.
|
||||
/*var currentUser = await _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User);
|
||||
if (currentUser != null)
|
||||
{
|
||||
_user = currentUser;
|
||||
|
||||
// Load managed event ids for current user.
|
||||
_managedEventIds = await GetUserAllowedEventIdsAsync(_user);
|
||||
|
||||
// Load all roles for current user.
|
||||
_userRoles = await _userManager.GetRolesAsync(User());
|
||||
|
||||
// Define if current user is admin.
|
||||
_isAdmin = _userRoles.Contains(AdminRole);
|
||||
}*/
|
||||
}
|
||||
|
||||
// Notify listeners that the user has been loaded.
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current logged on user based on HttpContext.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GenerateEmailConfirmTokenAsync()
|
||||
{
|
||||
return await _userManager.GenerateEmailConfirmationTokenAsync(User());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current logged on user based on HttpContext.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Claim>> GetCurrentUserClaimsAsync()
|
||||
{
|
||||
var claims = await _userManager.GetClaimsAsync(User());
|
||||
return claims;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current logged on user based on HttpContext.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<string>> GetCurrentUserRolesAsync()
|
||||
{
|
||||
var roles = await _userManager.GetRolesAsync(User());
|
||||
|
||||
return roles.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<AdminUser>> SearchUsersAsync(string searchTerm)
|
||||
{
|
||||
return await _userManager.Users.Where(x => x.UserName.Contains(searchTerm)).Take(5).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign out the current user.
|
||||
/// </summary>
|
||||
public async Task SignOutAsync()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> CreateUserAsync(AdminUser user, string password, List<string> roles)
|
||||
{
|
||||
var errors = await ValidateUser(user, password, isUpdate: false);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
var result = await _userManager.CreateAsync(user, password);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
errors.Add(error.Description);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
errors = await UpdateUserRolesAsync(user, roles);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public async Task<List<string>> UpdateUserAsync(AdminUser user, string newPassword)
|
||||
{
|
||||
var errors = await ValidateUser(user, newPassword, isUpdate: true);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Update password if necessary
|
||||
if (!string.IsNullOrEmpty(newPassword))
|
||||
{
|
||||
var passwordRemoveResult = await this._userManager.RemovePasswordAsync(user);
|
||||
if (!passwordRemoveResult.Succeeded)
|
||||
{
|
||||
foreach (var error in passwordRemoveResult.Errors)
|
||||
{
|
||||
errors.Add(error.Description);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
var passwordAddResult = await this._userManager.AddPasswordAsync(user, newPassword);
|
||||
if (!passwordAddResult.Succeeded)
|
||||
{
|
||||
foreach (var error in passwordAddResult.Errors)
|
||||
{
|
||||
errors.Add(error.Description);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
var result = await this._userManager.UpdateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
errors.Add(error.Description);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update user roles. This is a separate method because it is called from both CreateUserAsync and UpdateUserAsync.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="roles"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<List<string>> UpdateUserRolesAsync(AdminUser user, List<string> roles)
|
||||
{
|
||||
List<string> errors = new();
|
||||
|
||||
var currentRoles = await _userManager.GetRolesAsync(user);
|
||||
if (user.Id == User().Id && currentRoles.Contains(AdminRole) && !roles.Contains(AdminRole))
|
||||
{
|
||||
errors.Add("You cannot remove the Admin role from yourself if you are an Admin.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
var rolesToAdd = roles.Except(currentRoles).ToList();
|
||||
var rolesToRemove = currentRoles.Except(roles).ToList();
|
||||
|
||||
await this._userManager.AddToRolesAsync(user, rolesToAdd);
|
||||
await this._userManager.RemoveFromRolesAsync(user, rolesToRemove);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ValidateUser(AdminUser user, string password, bool isUpdate)
|
||||
{
|
||||
// Username and email are the same, so enforce any changes to username here to email as well
|
||||
user.Email = user.UserName;
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrEmpty(user.UserName) || string.IsNullOrEmpty(user.Email))
|
||||
{
|
||||
errors.Add("Username and email are required.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!new EmailAddressAttribute().IsValid(user.Email))
|
||||
{
|
||||
errors.Add("Email is not valid.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (isUpdate)
|
||||
{
|
||||
var originalUser = await this._userManager.FindByIdAsync(user.Id);
|
||||
if (user.UserName != originalUser.UserName)
|
||||
{
|
||||
errors.Add("Username cannot be changed for existing users.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingUser = await this._userManager.FindByNameAsync(user.UserName);
|
||||
if (existingUser != null)
|
||||
{
|
||||
errors.Add("Username is already in use.");
|
||||
}
|
||||
|
||||
var existingEmail = await this._userManager.FindByEmailAsync(user.Email);
|
||||
if (existingEmail != null)
|
||||
{
|
||||
errors.Add("Email is already in use.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
errors.Add("Password is required.");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public async Task<List<string>> DeleteUserAsync(AdminUser user, bool forceDelete = false)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Disallow deleting yourself, except when forceDelete is true
|
||||
if (user.Id == User().Id && !forceDelete)
|
||||
{
|
||||
errors.Add("You cannot delete yourself.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
// First delete all related data...
|
||||
// @TODO: do we not want to preserve certain anonymized data?
|
||||
|
||||
|
||||
// ...then delete the user
|
||||
var result = await _userManager.DeleteAsync(user);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
// Handle error, e.g. show error messages
|
||||
errors.Add("Unable to delete user.");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if supplied password is correct for the user.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> CheckPasswordAsync(AdminUser user, string password)
|
||||
{
|
||||
if (password.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _userManager.CheckPasswordAsync(user, password);
|
||||
}
|
||||
}
|
||||
38
src/AliasVault.Admin/Services/VersionedContentService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AliasVault.Admin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service to provide versioned content paths for cache busting of static files.
|
||||
/// </summary>
|
||||
public class VersionedContentService
|
||||
{
|
||||
private readonly string _webRootPath;
|
||||
|
||||
public VersionedContentService(string webRootPath)
|
||||
{
|
||||
_webRootPath = webRootPath ?? throw new ArgumentNullException(nameof(webRootPath));
|
||||
}
|
||||
|
||||
private Dictionary<string, string> _hashCache = new Dictionary<string, string>();
|
||||
|
||||
public string GetVersionedPath(string contentPath)
|
||||
{
|
||||
if (!_hashCache.TryGetValue(contentPath, out var version))
|
||||
{
|
||||
var serverPath = Path.Combine(_webRootPath, contentPath.TrimStart('/'));
|
||||
version = GetVersionHashFrom(serverPath);
|
||||
_hashCache[contentPath] = version;
|
||||
}
|
||||
|
||||
return $"{contentPath}?v={version}";
|
||||
}
|
||||
|
||||
private string GetVersionHashFrom(string serverPath)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
using var stream = File.OpenRead(serverPath);
|
||||
byte[] hash = md5.ComputeHash(stream);
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
27
src/AliasVault.Admin/StartupTasks.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace AliasVault.Admin;
|
||||
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Startup tasks that should be run when the application starts.
|
||||
/// </summary>
|
||||
public class StartupTasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the roles if they do not exist.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">IServiceProvider instance.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public static async Task CreateRolesIfNotExist(IServiceProvider serviceProvider)
|
||||
{
|
||||
var roleManager = serviceProvider.GetRequiredService<RoleManager<AdminRole>>();
|
||||
|
||||
const string adminRole = "Admin";
|
||||
|
||||
if (!await roleManager.RoleExistsAsync(adminRole))
|
||||
{
|
||||
await roleManager.CreateAsync(new AdminRole(adminRole));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/AliasVault.Admin/appsettings.Development.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AliasServerDbContext": "Data Source=../../database/AliasServerDb.sqlite"
|
||||
}
|
||||
}
|
||||
12
src/AliasVault.Admin/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"AliasServerDbContext": "Data Source=../../database/AliasServerDb.sqlite"
|
||||
}
|
||||
}
|
||||
178
src/AliasVault.Admin/wwwroot/app.css
Normal file
@@ -0,0 +1,178 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
|
||||
.nevocard-reconnect-modal > div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.components-reconnect-hide > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.components-reconnect-show > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.components-reconnect-show > .show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.components-reconnect-failed > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.components-reconnect-failed > .failed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.components-reconnect-rejected > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.components-reconnect-rejected > .rejected {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gradient-custom {
|
||||
/* fallback for old browsers */
|
||||
background: rgb(203, 116, 17);
|
||||
|
||||
/* Chrome 10-25, Safari 5.1-6 */
|
||||
background: -webkit-linear-gradient(
|
||||
to right, rgb(203, 116, 17),
|
||||
rgb(209, 200, 126)
|
||||
);
|
||||
|
||||
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
background: linear-gradient(
|
||||
to right, rgb(203, 116, 17),
|
||||
rgb(209, 200, 126)
|
||||
)
|
||||
}
|
||||
107
src/AliasVault.Admin/wwwroot/assets/css/demo.css
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* demo.css
|
||||
* File include item demo only specific css only
|
||||
******************************************************************************/
|
||||
|
||||
.menu .app-brand.demo {
|
||||
height: 64px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.app-brand-logo.demo svg {
|
||||
width: 22px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.app-brand-text.demo {
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.5px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* ! For .layout-navbar-fixed added fix padding top tpo .layout-page */
|
||||
/* Detached navbar */
|
||||
.layout-navbar-fixed .layout-wrapper:not(.layout-horizontal):not(.layout-without-menu) .layout-page {
|
||||
padding-top: 76px !important;
|
||||
}
|
||||
/* Default navbar */
|
||||
.layout-navbar-fixed .layout-wrapper:not(.layout-without-menu) .layout-page {
|
||||
padding-top: 64px !important;
|
||||
}
|
||||
|
||||
/* Navbar page z-index issue solution */
|
||||
.content-wrapper .navbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Content
|
||||
******************************************************************************/
|
||||
|
||||
.demo-blocks > * {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.demo-inline-spacing > * {
|
||||
margin: 1rem 0.375rem 0 0 !important;
|
||||
}
|
||||
|
||||
/* ? .demo-vertical-spacing class is used to have vertical margins between elements. To remove margin-top from the first-child, use .demo-only-element class with .demo-vertical-spacing class. For example, we have used this class in forms-input-groups.html file. */
|
||||
.demo-vertical-spacing > * {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.demo-vertical-spacing-lg > * {
|
||||
margin-top: 1.875rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing-lg.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.demo-vertical-spacing-xl > * {
|
||||
margin-top: 5rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing-xl.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.rtl-only {
|
||||
display: none !important;
|
||||
text-align: left !important;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rtl-only {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Layout demo
|
||||
******************************************************************************/
|
||||
|
||||
.layout-demo-wrapper {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.layout-demo-placeholder img {
|
||||
width: 900px;
|
||||
}
|
||||
.layout-demo-info {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
BIN
src/AliasVault.Admin/wwwroot/assets/img/avatars/1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/avatars/1.webp
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/avatars/5.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/avatars/6.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/avatars/7.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/backgrounds/18.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/1.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/11.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/12.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/13.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/17.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/18.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/19.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/2.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/20.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/3.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/4.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/5.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/elements/7.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/asana.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/behance.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 681 B |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/github.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/google.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/slack.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/brands/twitter.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 702 B |
|
After Width: | Height: | Size: 776 B |
|
After Width: | Height: | Size: 689 B |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/unicons/chart.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/unicons/paypal.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 936 B |
BIN
src/AliasVault.Admin/wwwroot/assets/img/icons/unicons/wallet.png
Normal file
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 62 KiB |
27
src/AliasVault.Admin/wwwroot/assets/js/config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Config
|
||||
* -------------------------------------------------------------------------------------
|
||||
* ! IMPORTANT: Make sure you clear the browser local storage In order to see the config changes in the template.
|
||||
* ! To clear local storage: (https://www.leadshook.com/help/how-to-clear-local-storage-in-google-chrome-browser/).
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// JS global variables
|
||||
let config = {
|
||||
colors: {
|
||||
primary: '#696cff',
|
||||
secondary: '#8592a3',
|
||||
success: '#71dd37',
|
||||
info: '#03c3ec',
|
||||
warning: '#ffab00',
|
||||
danger: '#ff3e1d',
|
||||
dark: '#233446',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
body: '#f4f5fb',
|
||||
headingColor: '#566a7f',
|
||||
axisColor: '#a1acb8',
|
||||
borderColor: '#eceef1'
|
||||
}
|
||||
};
|
||||