mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-31 10:09:22 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bca7b2bc82 | ||
|
|
d51ae8d913 | ||
|
|
c5d2b1da37 | ||
|
|
5d85b3a275 | ||
|
|
3ba8e54e56 | ||
|
|
7ffc1f1ee5 | ||
|
|
8d4024860b | ||
|
|
383145814a | ||
|
|
210f4b3c9e | ||
|
|
276ceb3dce | ||
|
|
2985c8333e | ||
|
|
7bb8aee532 | ||
|
|
7de3b05985 | ||
|
|
daca01a428 | ||
|
|
9fb19d28d6 | ||
|
|
540177c762 | ||
|
|
228b037a6d | ||
|
|
0e0366564d | ||
|
|
cca91d6076 | ||
|
|
6c9e770af7 | ||
|
|
44bcb7f16d | ||
|
|
d69b3defe5 | ||
|
|
02af26cb39 | ||
|
|
3cc3c67a4d | ||
|
|
107d2d8602 | ||
|
|
b8301d8f98 | ||
|
|
124491e5db | ||
|
|
dbea1c2c4d | ||
|
|
949a7a856a | ||
|
|
b923669b66 | ||
|
|
da25aa43ea |
@@ -10,10 +10,7 @@ insert_final_newline = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
@@ -30,7 +27,6 @@ csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
dotnet_diagnostic.SA1011.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
dotnet_diagnostic.SA1200.severity = none
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
dotnet_diagnostic.SA1310.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = none
|
||||
@@ -120,5 +116,8 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_coalesce_expression = false:suggestion
|
||||
dotnet_style_null_propagation = false:suggestion
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0046.severity = silent
|
||||
|
||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
JWT_KEY=
|
||||
49
.github/workflows/sonarcloud-code-analysis.yml
vendored
Normal file
49
.github/workflows/sonarcloud-code-analysis.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -370,3 +370,7 @@ FodyWeavers.xsd
|
||||
|
||||
.idea
|
||||
*.licenseheader
|
||||
|
||||
# AliasVault specific
|
||||
# index.html is generated by the build process from index.template.html and therefore should be ignored
|
||||
src/AliasVault.WebApp/wwwroot/index.html
|
||||
|
||||
61
README.md
61
README.md
@@ -1,12 +1,13 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>AliasVault</h1>
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-build-run-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-integration-tests.yml)
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-build-run-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-integration-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/coverage/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=test code coverage">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
</div>
|
||||
|
||||
> Disclaimer: This repository is currently in an alpha state and is NOT ready for production use. Critical features, such as encryption, are not yet fully implemented. AliasVault is a work in progress and as of this moment serves as a research playground. Users are welcome to explore and use this project, but please be aware that there are no guarantees regarding its security or stability. Use at your own risk!
|
||||
@@ -14,34 +15,56 @@
|
||||
AliasVault is an open-source password manager that can generate virtual identities complete with virtual email addresses. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
|
||||
## Features
|
||||
- Password Management: Securely store and manage your passwords.
|
||||
- Virtual Identities: Generate virtual identities with virtual (working) email addresses.
|
||||
- Data Protection: Ensures that all sensitive data is encrypted and securely stored.
|
||||
- User Authentication: Secure login and user management functionalities.
|
||||
- **Password Management:** Securely store and manage your passwords.
|
||||
- **Virtual Identities:** Generate virtual identities with virtual (working) email addresses that are assigned to one or more passwords.
|
||||
- **Zero-knowledge architecture:** Ensures that all sensitive data is end-to-end encrypted on the client and stored in encrypted state on the database. The server never has access to your data.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository.
|
||||
### 1. Clone this repository.
|
||||
|
||||
```bash
|
||||
git clone [URL]
|
||||
# Clone this Git repository to "AliasVault" directory
|
||||
$ git clone https://github.com/lanedirt/AliasVault.git
|
||||
```
|
||||
|
||||
2. Run the app via Docker:
|
||||
### 2. Run the init script to set up the .env file and generate a random encryption secret.
|
||||
This script will create a .env file in the root directory of the project if it does not yet exist and populate it with a random encryption secret.
|
||||
```bash
|
||||
# Go to the project directory
|
||||
$ cd AliasVault
|
||||
|
||||
# Make init script executable
|
||||
$ chmod +x init.sh
|
||||
|
||||
# Run the init script
|
||||
$ ./init.sh
|
||||
```
|
||||
|
||||
### 3. Build and run the app via Docker:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build --force-recreate
|
||||
# Build and run the app via Docker Compose
|
||||
$ docker compose up -d --build --force-recreate
|
||||
```
|
||||
|
||||
The app will be available at http://localhost:80
|
||||
> Note: the container binds to port 80 by default. If you have another service running on port 80, you can change the port in the `docker-compose.yml` file.
|
||||
|
||||
|
||||
#### Note for first time build:
|
||||
- When running the app for the first time, it may take a few minutes to build the Docker image.
|
||||
- A SQLite database file will be created in `./database/aliasdb.sqlite`. This file will store all (encrypted) password vaults. It should be kept secure and not shared.
|
||||
|
||||
## Credits
|
||||
The following libraries and frameworks are used in this project:
|
||||
After the Docker containers have started the app will be available at http://localhost:80
|
||||
|
||||
## Tech stack / credits
|
||||
The following technologies, frameworks and libraries are used in this project:
|
||||
|
||||
- [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) - A simple, modern, object-oriented, and type-safe programming language.
|
||||
- [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) - An open-source framework for building modern, cloud-based, internet-connected applications.
|
||||
- [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript.
|
||||
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) - A lightweight, extensible, open-source and cross-platform version of the popular Entity Framework data access technology.
|
||||
- [Blazor WASM](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript. It's a single-page app framework that runs in the browser via WebAssembly.
|
||||
- [Docker](https://www.docker.com/) - A platform for building, sharing, and running containerized applications.
|
||||
- [SQLite](https://www.sqlite.org/index.html) - A C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine.
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library.
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library based on Tailwind CSS.
|
||||
- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm.
|
||||
|
||||
@@ -30,4 +30,6 @@ services:
|
||||
- "81:8081"
|
||||
volumes:
|
||||
- ./database:/database
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
78
init.sh
Executable file
78
init.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Define colors for CLI output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Define the path to the .env and .env.example files
|
||||
ENV_FILE=".env"
|
||||
ENV_EXAMPLE_FILE=".env.example"
|
||||
|
||||
# Function to generate a new 32-character JWT key
|
||||
generate_jwt_key() {
|
||||
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 | head -c 32
|
||||
}
|
||||
|
||||
# Function to create .env file from .env.example if it doesn't exist
|
||||
create_env_file() {
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf "${GREEN}> .env file created from .env.example.${NC}\n"
|
||||
else
|
||||
touch "$ENV_FILE"
|
||||
printf "${YELLOW}> .env file created as empty because .env.example was not found.${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf "${CYAN}> .env file already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check and populate the .env file with JWT_KEY
|
||||
populate_jwt_key() {
|
||||
if ! grep -q "^JWT_KEY=" "$ENV_FILE" || [ -z "$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "${YELLOW}JWT_KEY not found or empty in $ENV_FILE. Generating a new JWT key...${NC}\n"
|
||||
JWT_KEY=$(generate_jwt_key)
|
||||
if grep -q "^JWT_KEY=" "$ENV_FILE"; then
|
||||
awk -v key="$JWT_KEY" '/^JWT_KEY=/ {$0="JWT_KEY="key} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
printf "JWT_KEY=${JWT_KEY}" >> "$ENV_FILE\n"
|
||||
fi
|
||||
printf "${GREEN}> JWT_KEY has been added to $ENV_FILE.${NC}\n"
|
||||
else
|
||||
printf "${CYAN}> JWT_KEY already exists and has a value in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print the CLI logo
|
||||
print_logo() {
|
||||
printf "${MAGENTA}\n"
|
||||
printf "=========================================================\n"
|
||||
printf " _ _ __ __ _ _ \n"
|
||||
printf " /\ | (_) \ \ / / | | | \n"
|
||||
printf " / \ | |_ __ _ __\ \ / /_ _ _ _| | |_\n"
|
||||
printf " / /\ \ | | |/ _ / __\ \/ / _ | | | | | __|\n"
|
||||
printf " / ____ \| | | (_| \__ \\ / (_| | |_| | | |_ \n"
|
||||
printf " /_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf "\n"
|
||||
printf "=========================================================\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
# Run the functions and print status
|
||||
print_logo
|
||||
printf "${BLUE}Initializing AliasVault...${NC}\n"
|
||||
create_env_file
|
||||
populate_jwt_key
|
||||
printf "${BLUE}Initialization complete.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To build the images and start the containers, run the following command:\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}$ docker compose up -d --build --force-recreate${NC}\n"
|
||||
printf "\n"
|
||||
printf "\n"
|
||||
@@ -60,16 +60,17 @@ public class AliasDbContext : IdentityDbContext
|
||||
/// <summary>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">ModelBuilder instance.</param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
/// <param name="builder">ModelBuilder instance.</param>
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
base.OnModelCreating(builder);
|
||||
foreach (var entity in builder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entity.GetProperties())
|
||||
{
|
||||
// TODO: This is a workaround for SQLite. Add conditional check if SQLite is used.
|
||||
// TODO: SQL server doesn't need this override.
|
||||
// NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
|
||||
// NOTE: SQL server doesn't need this override.
|
||||
|
||||
// SQLite does not support varchar(max) so we use TEXT.
|
||||
if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
|
||||
{
|
||||
@@ -79,42 +80,42 @@ public class AliasDbContext : IdentityDbContext
|
||||
}
|
||||
|
||||
// Configure Identity - Login relationship
|
||||
modelBuilder.Entity<Login>()
|
||||
builder.Entity<Login>()
|
||||
.HasOne(l => l.Identity)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.IdentityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure the Login - UserId entity
|
||||
modelBuilder.Entity<Login>()
|
||||
builder.Entity<Login>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
|
||||
// Configure Login - Service relationship
|
||||
modelBuilder.Entity<Login>()
|
||||
builder.Entity<Login>()
|
||||
.HasOne(l => l.Service)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ServiceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure Login - Password relationship
|
||||
modelBuilder.Entity<Login>()
|
||||
builder.Entity<Login>()
|
||||
.HasMany(l => l.Passwords)
|
||||
.WithOne(p => p.Login)
|
||||
.HasForeignKey(p => p.LoginId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure Identity - DefaultPassword relationship
|
||||
modelBuilder.Entity<Identity>()
|
||||
builder.Entity<Identity>()
|
||||
.HasOne(i => i.DefaultPassword)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.DefaultPasswordId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Configure the User - AspNetUserRefreshToken entity
|
||||
modelBuilder.Entity<AspNetUserRefreshToken>()
|
||||
builder.Entity<AspNetUserRefreshToken>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ public class FigIdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
private static readonly string Url = "https://api.identiteitgenerator.nl/generate/identity";
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
|
||||
@@ -23,10 +27,7 @@ public class FigIdentityGenerator : IIdentityGenerator
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, JsonSerializerOptions);
|
||||
|
||||
if (identity is null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -18,6 +18,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace AliasVault.Api.Controllers;
|
||||
using System.Globalization;
|
||||
using AliasDb;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -21,6 +22,7 @@ using Service = AliasVault.Shared.Models.WebApi.Service;
|
||||
/// </summary>
|
||||
/// <param name="context">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class AliasController(AliasDbContext context, UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------s
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
@@ -13,6 +13,8 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AliasDb;
|
||||
using AliasVault.Shared.Models;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@@ -24,8 +26,9 @@ using Microsoft.IdentityModel.Tokens;
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="signInManager">SignInManager instance.</param>
|
||||
/// <param name="configuration">IConfiguration instance.</param>
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
public class AuthController(AliasDbContext context, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IConfiguration configuration) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -39,11 +42,11 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
var user = await userManager.FindByEmailAsync(model.Email);
|
||||
if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
var tokenModel = await GenerateNewTokenForUser(user);
|
||||
var tokenModel = await GenerateNewTokensForUser(user);
|
||||
return Ok(tokenModel);
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
return BadRequest(ServerValidationErrorResponse.Create("Invalid username or password. Please try again.", 400));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,15 +151,40 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
await signInManager.SignInAsync(user, isPersistent: false);
|
||||
|
||||
// Return the token.
|
||||
var tokenModel = await GenerateNewTokenForUser(user);
|
||||
var tokenModel = await GenerateNewTokensForUser(user);
|
||||
return Ok(tokenModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(result.Errors);
|
||||
}
|
||||
|
||||
var errors = result.Errors.Select(e => e.Description).ToArray();
|
||||
return BadRequest(ServerValidationErrorResponse.Create(errors, 400));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a device identifier based on request headers. This is used to associate refresh tokens
|
||||
/// with a specific device for a specific user.
|
||||
///
|
||||
/// NOTE: current implementation means that only one refresh token can be valid for a
|
||||
/// specific user/device combo at a time. The identifier generation could be made more unique in the future
|
||||
/// to prevent any unwanted conflicts.
|
||||
/// </summary>
|
||||
/// <param name="request">The HttpRequest instance for the request that the client used.</param>
|
||||
/// <returns>Unique device identifier as string.</returns>
|
||||
private static string GenerateDeviceIdentifier(HttpRequest request)
|
||||
{
|
||||
var userAgent = request.Headers.UserAgent.ToString();
|
||||
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
|
||||
|
||||
var rawIdentifier = $"{userAgent}|{acceptLanguage}";
|
||||
return rawIdentifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a Jwt access token for a user. This token is used to authenticate the user for a limited time
|
||||
/// and is short-lived by design. With the separate refresh token, the user can request a new access token
|
||||
/// when this access token expires.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the Jwt access token for.</param>
|
||||
/// <returns>Access token as string.</returns>
|
||||
private string GenerateJwtToken(IdentityUser user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
@@ -167,19 +195,30 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty));
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
audience: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(30),
|
||||
expires: DateTime.Now.AddMinutes(10),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a refresh token for a user. This token is used to request a new access token when the current
|
||||
/// access token expires. The refresh token is long-lived by design.
|
||||
/// </summary>
|
||||
/// <returns>Random string to be used as refresh token.</returns>
|
||||
private string GenerateRefreshToken()
|
||||
{
|
||||
var randomNumber = new byte[32];
|
||||
@@ -210,17 +249,13 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
return principal;
|
||||
}
|
||||
|
||||
private string GenerateDeviceIdentifier(HttpRequest request)
|
||||
{
|
||||
// TODO: Add more headers to the device identifier or let client send a unique identifier instead.
|
||||
var userAgent = request.Headers.UserAgent.ToString();
|
||||
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
|
||||
|
||||
var rawIdentifier = $"{userAgent}|{acceptLanguage}";
|
||||
return rawIdentifier;
|
||||
}
|
||||
|
||||
private async Task<TokenModel> GenerateNewTokenForUser(IdentityUser user)
|
||||
/// <summary>
|
||||
/// Generates a new access and refresh token for a user and persists the refresh token
|
||||
/// to the database.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the tokens for.</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token.</returns>
|
||||
private async Task<TokenModel> GenerateNewTokensForUser(IdentityUser user)
|
||||
{
|
||||
var token = GenerateJwtToken(user);
|
||||
var refreshToken = GenerateRefreshToken();
|
||||
|
||||
@@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
/// Base controller for requests that require authentication.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class AuthenticatedRequestController(UserManager<IdentityUser> userManager) : ControllerBase
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace AliasVault.Api.Controllers;
|
||||
|
||||
using AliasGenerators.Identity;
|
||||
using AliasGenerators.Identity.Implementations;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
/// Controller for identity generation.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class IdentityController(UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -30,7 +32,7 @@ public class IdentityController(UserManager<IdentityUser> userManager) : Authent
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
IIdentityGenerator identityGenerator = new FigIdentityGenerator();
|
||||
var identityGenerator = new FigIdentityGenerator();
|
||||
return Ok(await identityGenerator.GenerateRandomIdentityAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ COPY . .
|
||||
|
||||
# Build the WebApi project
|
||||
WORKDIR "/src/src/AliasVault.Api"
|
||||
RUN dotnet build "AliasVault.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
|
||||
# Publish the application to the /app/publish directory in the container
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Create the migration bundle
|
||||
# Install the Entity Framework Core CLI tool and run migrations to create the database
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Data.Common;
|
||||
using System.Text;
|
||||
using AliasDb;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@@ -26,7 +27,6 @@ builder.Services.AddLogging(logging =>
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddSingleton<DbConnection>(container =>
|
||||
{
|
||||
var configFile = new ConfigurationBuilder()
|
||||
@@ -73,6 +73,12 @@ builder.Services.AddAuthentication(options =>
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
options.IncludeErrorDetails = true;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
@@ -83,7 +89,7 @@ builder.Services.AddAuthentication(options =>
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = configuration["Jwt:Issuer"],
|
||||
ValidAudience = configuration["Jwt:Issuer"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty)),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
};
|
||||
});
|
||||
@@ -99,7 +105,19 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AliasVault API", Version = "v1" });
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:39952",
|
||||
"sslPort": 44368
|
||||
}
|
||||
},
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5092",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"JWT_KEY": "12345678901234567890123456789012"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5092"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7223;http://localhost:5092",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"JWT_KEY": "12345678901234567890123456789012"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7223;http://localhost:5092"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
@@ -37,5 +30,14 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:39952",
|
||||
"sslPort": 44368
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
}
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "[[&lokl$4r<ak{f}4d#iv7>92i*)=sfo",
|
||||
"Issuer": "YourIssuer"
|
||||
"Issuer": "AliasVault"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AliasDbContext": "Data Source=../../database/aliasdb.sqlite"
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Login model.
|
||||
/// </summary>
|
||||
@@ -15,10 +17,13 @@ public class LoginModel
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasVault.Shared.Models.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Register model.
|
||||
/// </summary>
|
||||
@@ -15,20 +18,27 @@ public class RegisterModel
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password confirmation.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string PasswordConfirm { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
|
||||
/// </summary>
|
||||
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
|
||||
public bool AcceptTerms { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MustBeTrueAttribute.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Validation attribute to ensure that a boolean property is true.
|
||||
/// </summary>
|
||||
public class MustBeTrueAttribute : ValidationAttribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
return false;
|
||||
case bool b:
|
||||
return b;
|
||||
default:
|
||||
throw new InvalidOperationException("Can only be used on boolean properties.");
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/AliasVault.Shared/Models/WebApi/AliasEdit.cs
Normal file
47
src/AliasVault.Shared/Models/WebApi/AliasEdit.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AliasEdit.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Alias model.
|
||||
/// </summary>
|
||||
public class AliasEdit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the service.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ServiceName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the service.
|
||||
/// </summary>
|
||||
public string? ServiceUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias Identity object.
|
||||
/// </summary>
|
||||
public Identity Identity { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias Password object.
|
||||
/// </summary>
|
||||
public Password Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias CreateDate.
|
||||
/// </summary>
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias LastUpdate.
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerValidationErrorResponse.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the structure of a validation error response from the API.
|
||||
/// </summary>
|
||||
public class ServerValidationErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP status code of the response.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation errors. The key is the name of the field that has the error, and the value is an array of error messages for that field.
|
||||
/// </summary>
|
||||
[JsonPropertyName("errors")]
|
||||
public Dictionary<string, string[]> Errors { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trace ID of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string TraceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">Title of the error.</param>
|
||||
/// <param name="status">Status code.</param>
|
||||
/// <returns>ServerValidationErrorResponse object.</returns>
|
||||
public static ServerValidationErrorResponse Create(string title, int status)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>
|
||||
{
|
||||
{ title, [title] },
|
||||
};
|
||||
|
||||
return new ServerValidationErrorResponse
|
||||
{
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
Title = title,
|
||||
Errors = errors,
|
||||
Status = status,
|
||||
TraceId = Guid.NewGuid().ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="errorArray">Array with errors.</param>
|
||||
/// <param name="status">Status code.</param>
|
||||
/// <returns>ServerValidationErrorResponse object.</returns>
|
||||
public static ServerValidationErrorResponse Create(string[] errorArray, int status)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>();
|
||||
foreach (var t in errorArray)
|
||||
{
|
||||
errors.Add(t, new[] { t });
|
||||
}
|
||||
|
||||
return new ServerValidationErrorResponse
|
||||
{
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
Title = errorArray[0],
|
||||
Errors = errors,
|
||||
Status = status,
|
||||
TraceId = Guid.NewGuid().ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Service model.
|
||||
/// </summary>
|
||||
@@ -15,6 +17,7 @@ public class Service
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the service.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,11 +5,21 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<BuildVersion>$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))</BuildVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GenerateCacheBustedIndexHtml" BeforeTargets="Build">
|
||||
<Exec Command="dotnet msbuild -t:ReplaceText -p:CacheBuster=$(CacheBuster);BuildVersion='$(BuildVersion)'" />
|
||||
</Target>
|
||||
|
||||
<Target Name="ReplaceText">
|
||||
<Exec Command="sed 's/@CacheBuster/$(CacheBuster)/g;s/@BuildVersion/$(BuildVersion)/g' wwwroot/index.template.html > wwwroot/index.html" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CacheBuster>dev</CacheBuster>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
@@ -17,6 +27,7 @@
|
||||
<DocumentationFile>bin\Release\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Optimize>True</Optimize>
|
||||
<CacheBuster>$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</CacheBuster>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@page "/user/login"
|
||||
@attribute [AllowAnonymous]
|
||||
@layout Auth.Layout.MainLayout
|
||||
|
||||
@inject HttpClient Http
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
@using AliasVault.WebApp.Auth.Components
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
|
||||
@@ -15,14 +15,20 @@
|
||||
Sign in to AliasVault
|
||||
</h2>
|
||||
|
||||
<EditForm Model="_user" OnSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
|
||||
<ServerValidationErrors @ref="_serverValidationErrors" />
|
||||
|
||||
<EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="_user.Email" placeholder="name@company.com" />
|
||||
<InputTextField id="email" @bind-Value="_loginModel.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => _loginModel.Email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="_user.Password" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => _loginModel.Password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
@@ -41,12 +47,10 @@
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
|
||||
|
||||
@code {
|
||||
private LoginModel _user = new LoginModel();
|
||||
private readonly LoginModel _loginModel = new();
|
||||
private FullScreenLoadingIndicator _loadingIndicator = new();
|
||||
private ServerValidationErrors _serverValidationErrors = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -62,11 +66,20 @@
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Http.PostAsJsonAsync("api/Auth/login", _user);
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", _loginModel);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
_serverValidationErrors.ParseResponse(responseContent);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
@@ -80,6 +93,7 @@
|
||||
{
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
return;
|
||||
}
|
||||
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@page "/user/logout"
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
@attribute [AllowAnonymous]
|
||||
@layout Auth.Layout.MainLayout
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
@@ -13,6 +14,7 @@
|
||||
await base.OnInitializedAsync();
|
||||
await AuthService.RemoveTokensAsync();
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to home page
|
||||
NavigationManager.NavigateTo("/");
|
||||
|
||||
@@ -14,26 +14,34 @@
|
||||
Create a Free Account
|
||||
</h2>
|
||||
|
||||
<EditForm Model="user" OnSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
|
||||
<ServerValidationErrors @ref="_serverValidationErrors" />
|
||||
|
||||
<EditForm Model="_registerModel" OnValidSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="user.Email" placeholder="name@company.com" />
|
||||
<InputTextField id="email" @bind-Value="_registerModel.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => _registerModel.Email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="user.Password" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password" @bind-Value="_registerModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => _registerModel.Password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<InputTextField id="password2" @bind-Value="user.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password2" @bind-Value="_registerModel.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => _registerModel.PasswordConfirm"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="terms" aria-describedby="terms" name="terms" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" required>
|
||||
<InputCheckbox id="terms" @bind-Value="_registerModel.AcceptTerms" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="terms" class="font-medium text-gray-900 dark:text-white">I accept the <a href="#" class="text-primary-700 hover:underline dark:text-primary-500">Terms and Conditions</a></label>
|
||||
<ValidationMessage For="() => _registerModel.AcceptTerms"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,79 +51,48 @@
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (validationErrors.Any())
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach (var error in validationErrors)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<FullScreenLoadingIndicator @ref="loadingIndicator" />
|
||||
|
||||
@code {
|
||||
RegisterModel user = new();
|
||||
FullScreenLoadingIndicator loadingIndicator = new();
|
||||
List<string> validationErrors = [];
|
||||
private readonly RegisterModel _registerModel = new();
|
||||
private FullScreenLoadingIndicator _loadingIndicator = new();
|
||||
private ServerValidationErrors _serverValidationErrors = new();
|
||||
|
||||
async Task HandleRegister()
|
||||
{
|
||||
loadingIndicator.Show();
|
||||
validationErrors.Clear();
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Http.PostAsJsonAsync("api/Auth/register", user);
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/register", _registerModel);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
_serverValidationErrors.ParseResponse(responseContent);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenObject != null)
|
||||
{
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
}
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
if (tokenObject != null)
|
||||
{
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ValidationErrorResponse>(responseContent);
|
||||
if (errorResponse != null && errorResponse.Errors != null)
|
||||
{
|
||||
foreach (var error in errorResponse.Errors.Values)
|
||||
{
|
||||
validationErrors.AddRange(error);
|
||||
}
|
||||
}
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingIndicator.Hide();
|
||||
_loadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidationErrorResponse
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public string Title { get; set; } = null!;
|
||||
public int Status { get; set; }
|
||||
public Dictionary<string, string[]> Errors { get; set; } = new();
|
||||
public string TraceId { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class AuthStateProvider(AuthService authService) : AuthenticationStatePro
|
||||
var jsonBytes = ParseBase64WithoutPadding(payload);
|
||||
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
||||
|
||||
if (keyValuePairs == null)
|
||||
if (keyValuePairs is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse JWT token.");
|
||||
}
|
||||
|
||||
@@ -16,23 +16,15 @@ using Blazored.LocalStorage;
|
||||
/// This service is responsible for handling authentication-related operations such as refreshing tokens,
|
||||
/// storing tokens, and revoking tokens.
|
||||
/// </summary>
|
||||
public class AuthService
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="AuthService"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="httpClient">The HTTP client.</param>
|
||||
/// <param name="localStorage">The local storage service.</param>
|
||||
public class AuthService(HttpClient httpClient, ILocalStorageService localStorage)
|
||||
{
|
||||
private const string AccessTokenKey = "token";
|
||||
private const string RefreshTokenKey = "refreshToken";
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">The HTTP client.</param>
|
||||
/// <param name="localStorage">The local storage service.</param>
|
||||
public AuthService(HttpClient httpClient, ILocalStorageService localStorage)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_localStorage = localStorage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the access token asynchronously.
|
||||
@@ -44,14 +36,14 @@ public class AuthService
|
||||
var accessToken = await GetAccessTokenAsync();
|
||||
var refreshToken = await GetRefreshTokenAsync();
|
||||
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/refresh")
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/refresh")
|
||||
{
|
||||
Content = JsonContent.Create(tokenInput),
|
||||
};
|
||||
|
||||
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
|
||||
request.Headers.Add("X-Ignore-Failure", "true");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -77,7 +69,7 @@ public class AuthService
|
||||
/// <returns>The stored access token.</returns>
|
||||
public async Task<string> GetAccessTokenAsync()
|
||||
{
|
||||
return await _localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
|
||||
return await localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,7 +79,7 @@ public class AuthService
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task StoreAccessTokenAsync(string newToken)
|
||||
{
|
||||
await _localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
|
||||
await localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -96,7 +88,7 @@ public class AuthService
|
||||
/// <returns>The stored refresh token.</returns>
|
||||
public async Task<string> GetRefreshTokenAsync()
|
||||
{
|
||||
return await _localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
|
||||
return await localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,7 +98,7 @@ public class AuthService
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task StoreRefreshTokenAsync(string newToken)
|
||||
{
|
||||
await _localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
|
||||
await localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,8 +107,8 @@ public class AuthService
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task RemoveTokensAsync()
|
||||
{
|
||||
await _localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await _localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
await localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
|
||||
// If the remote call fails we catch the exception and ignore it.
|
||||
// This is because the user is already logged out and we don't want to trigger another refresh token request.
|
||||
@@ -142,13 +134,13 @@ public class AuthService
|
||||
RefreshToken = await GetRefreshTokenAsync(),
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/revoke")
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/revoke")
|
||||
{
|
||||
Content = JsonContent.Create(tokenInput),
|
||||
};
|
||||
|
||||
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
|
||||
request.Headers.Add("X-Ignore-Failure", "true");
|
||||
await _httpClient.SendAsync(request);
|
||||
await httpClient.SendAsync(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The message to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The message to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
|
||||
@@ -48,20 +48,20 @@
|
||||
/// <summary>
|
||||
/// Refreshes the messages by adding any new messages from the PortalMessageService.
|
||||
/// </summary>
|
||||
public void RefreshAddMessages()
|
||||
private void RefreshAddMessages()
|
||||
{
|
||||
// We retrieve any additional messages from the GlobalNotificationService that we do not yet have.
|
||||
var newMessages = GlobalNotificationService.GetMessagesForDisplay();
|
||||
foreach (var message in newMessages)
|
||||
{
|
||||
if (!Messages.Any(m => m.Key == message.Key && m.Value == message.Value))
|
||||
if (!Messages.Exists(m => m.Key == message.Key && m.Value == message.Value))
|
||||
{
|
||||
Messages.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove messages that are no longer in the GlobalNotificationService and have already been displayed.
|
||||
var messagesToRemove = Messages.Where(m => !newMessages.Any(nm => nm.Key == m.Key && nm.Value == m.Value)).ToList();
|
||||
var messagesToRemove = Messages.Where(m => !newMessages.Exists(nm => nm.Key == m.Key && nm.Value == m.Value)).ToList();
|
||||
foreach (var message in messagesToRemove)
|
||||
{
|
||||
Messages.Remove(message);
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{
|
||||
base.OnInitialized();
|
||||
// Remove first item if it is the home page
|
||||
if (BreadcrumbItems.Any() && BreadcrumbItems.First().DisplayName == "Home")
|
||||
if (BreadcrumbItems.Any() && BreadcrumbItems[0].DisplayName == "Home")
|
||||
{
|
||||
BreadcrumbItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
[Parameter] public string Value { get; set; } = string.Empty;
|
||||
|
||||
private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId;
|
||||
private string _inputId = Guid.NewGuid().ToString();
|
||||
private readonly string _inputId = Guid.NewGuid().ToString();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
|
||||
@if (_errors.Any())
|
||||
{
|
||||
@foreach (var error in _errors)
|
||||
{
|
||||
<AlertMessageError Message="@error" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<string> _errors = [];
|
||||
|
||||
/// <summary>
|
||||
/// Parses the response content and displays the server validation errors.
|
||||
/// </summary>
|
||||
public void ParseResponse(string responseContent)
|
||||
{
|
||||
_errors.Clear();
|
||||
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ServerValidationErrorResponse>(responseContent);
|
||||
if (errorResponse is not null)
|
||||
{
|
||||
foreach (var error in errorResponse.Errors)
|
||||
{
|
||||
_errors.AddRange(error.Value);
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the server validation errors.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_errors.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,12 @@ COPY . .
|
||||
|
||||
# Build the WebApp project
|
||||
WORKDIR "/src/src/AliasVault.WebApp"
|
||||
RUN dotnet build "AliasVault.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "AliasVault.WebApp.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
|
||||
# Publish the WebApp project
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "AliasVault.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "AliasVault.WebApp.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Final stage: start nginx and serve static html files that were published in the previous stage
|
||||
FROM nginx:alpine AS final
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@inherits PageBase
|
||||
@implements IDisposable
|
||||
|
||||
<header>
|
||||
<nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4">
|
||||
@@ -41,9 +42,9 @@
|
||||
|
||||
@if (isMenuOpen)
|
||||
{
|
||||
<div class="z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom" style="position: absolute; inset: 0px auto auto 0px; margin: 0px; margin-left: -200px; transform: translate3d(1537.5px, 58px, 0px);">
|
||||
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom">
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@_username</span>
|
||||
</div>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
@@ -58,73 +59,84 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<button type="button" id="toggleMobileMenuButton" data-collapse-toggle="toggleMobileMenu" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600">
|
||||
<button @onclick="ToggleMobileMenu" type="button" id="toggleMobileMenuButton" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open menu</span>
|
||||
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="bg-white dark:bg-gray-900">
|
||||
|
||||
<ul id="toggleMobileMenu" class="hidden flex-col mt-0 pt-16 w-full text-sm font-medium lg:hidden">
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" aria-current="page">Home</a>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Messages</a>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Profile</a>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Settings</a>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<button type="button" data-collapse-toggle="dropdownMobileNavbar" class="flex justify-between items-center py-3 px-4 w-full text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Dropdown <svg class="w-6 h-6 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg></button>
|
||||
<ul id="dropdownMobileNavbar" class="hidden">
|
||||
<li class="block border-t border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Item 1</a>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Item 2</a>
|
||||
</li>
|
||||
<li class="block">
|
||||
<a href="#" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0">Item 3</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@if (isMobileMenuOpen)
|
||||
{
|
||||
<nav class="bg-white dark:bg-gray-900">
|
||||
<ul id="mobileMenu" class="flex-col mt-0 pt-16 w-full text-sm font-medium lg:hidden">
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="/" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="/aliases" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Aliases
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
</header>
|
||||
|
||||
@code {
|
||||
private bool isMenuOpen = false;
|
||||
public string Username { get; set; } = "";
|
||||
private bool isMobileMenuOpen = false;
|
||||
private string _username { get; set; } = "";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
Username = await GetUsernameAsync();
|
||||
_username = await GetUsernameAsync();
|
||||
NavigationManager.LocationChanged += LocationChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
await Js.InvokeVoidAsync("window.initTopMenu");
|
||||
DotNetObjectReference<TopMenu> objRef = DotNetObjectReference.Create(this);
|
||||
await Js.InvokeVoidAsync("window.registerClickOutsideHandler", objRef);
|
||||
}
|
||||
}
|
||||
|
||||
void LocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
isMenuOpen = false;
|
||||
isMobileMenuOpen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ToggleMenu()
|
||||
{
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
|
||||
private void CloseMenu()
|
||||
private void ToggleMobileMenu()
|
||||
{
|
||||
isMobileMenuOpen = !isMobileMenuOpen;
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void CloseMenu()
|
||||
{
|
||||
isMenuOpen = false;
|
||||
isMobileMenuOpen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= LocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@using AliasGenerators.Implementations
|
||||
@using AliasGenerators.Password.Implementations
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
@using System.Globalization
|
||||
|
||||
@if (EditMode)
|
||||
{
|
||||
@@ -43,103 +44,107 @@ else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="service-name" Label="Service Name" Value="@(Obj.Service.Name)" ValueChanged="@(val => Obj.Service.Name = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="service-url" Label="Service URL" Value="@(Obj.Service.Url)" ValueChanged="@(val => Obj.Service.Url = val)"></EditFormRow>
|
||||
<EditForm Model="Obj" OnValidSubmit="SaveAlias">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="service-name" Label="Service Name" @bind-Value="Obj.ServiceName"></EditFormRow>
|
||||
<ValidationMessage For="() => Obj.ServiceName"/>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="service-url" Label="Service URL" @bind-Value="Obj.ServiceUrl"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
|
||||
<div class="mb-4">
|
||||
<button class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
|
||||
<button class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800" @onclick="SaveAlias">Save Alias</button>
|
||||
@if (IsIdentityLoading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
</div>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="email" Label="Email" Value="@(Obj.Identity.EmailPrefix)" ValueChanged="@(val => Obj.Identity.EmailPrefix = val)"></EditFormRow>
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
|
||||
<div class="mb-4">
|
||||
<button type="button" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
|
||||
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Alias</button>
|
||||
@if (IsIdentityLoading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="username" Label="Username" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="password" Label="Password" Value="@(Obj.Password.Value)" ValueChanged="@(val => Obj.Password.Value = val)"></EditFormRow>
|
||||
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="email" Label="Email" @bind-Value="Obj.Identity.EmailPrefix"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Identity.NickName"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
|
||||
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="first-name" Label="First Name" Value="@(Obj.Identity.FirstName)" ValueChanged="@(val => Obj.Identity.FirstName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="last-name" Label="Last Name" Value="@(Obj.Identity.LastName)" ValueChanged="@(val => Obj.Identity.LastName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="gender" Label="Gender" Value="@(Obj.Identity.Gender)" ValueChanged="@(val => Obj.Identity.Gender = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="nickname" Label="Nick Name" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="birthdate" Label="Birth Date" Value="@(Obj.Identity.BirthDate)" ValueChanged="@(val => Obj.Identity.BirthDate = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="street" Label="Address Street" Value="@(Obj.Identity.AddressStreet)" ValueChanged="@(val => Obj.Identity.AddressStreet = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="city" Label="Address City" Value="@(Obj.Identity.AddressCity)" ValueChanged="@(val => Obj.Identity.AddressCity = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="state" Label="Address State" Value="@(Obj.Identity.AddressState)" ValueChanged="@(val => Obj.Identity.AddressState = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="zipcode" Label="Address Zip Code" Value="@(Obj.Identity.AddressZipCode)" ValueChanged="@(val => Obj.Identity.AddressZipCode = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="country" Label="Address Country" Value="@(Obj.Identity.AddressCountry)" ValueChanged="@(val => Obj.Identity.AddressCountry = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="hobbies" Label="Hobbies" Value="@(Obj.Identity.Hobbies)" ValueChanged="@(val => Obj.Identity.Hobbies = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="phone-mobile" Label="Phone Mobile" Value="@(Obj.Identity.PhoneMobile)" ValueChanged="@(val => Obj.Identity.PhoneMobile = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="iban" Label="Bank Account IBAN" Value="@(Obj.Identity.BankAccountIBAN)" ValueChanged="@(val => Obj.Identity.BankAccountIBAN = val)"></EditFormRow>
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="first-name" Label="First Name" @bind-Value="Obj.Identity.FirstName"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="last-name" Label="Last Name" @bind-Value="Obj.Identity.LastName"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="gender" Label="Gender" @bind-Value="Obj.Identity.Gender"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="nickname" Label="Nick Name" @bind-Value="Obj.Identity.NickName"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="birthdate" Label="Birth Date" @bind-Value="Obj.Identity.BirthDate"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="street" Label="Address Street" @bind-Value="Obj.Identity.AddressStreet"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="city" Label="Address City" @bind-Value="Obj.Identity.AddressCity"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="state" Label="Address State" @bind-Value="Obj.Identity.AddressState"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="zipcode" Label="Address Zip Code" @bind-Value="Obj.Identity.AddressZipCode"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="country" Label="Address Country" @bind-Value="Obj.Identity.AddressCountry"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="hobbies" Label="Hobbies" @bind-Value="Obj.Identity.Hobbies"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="phone-mobile" Label="Phone Mobile" @bind-Value="Obj.Identity.PhoneMobile"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="iban" Label="Bank Account IBAN" @bind-Value="Obj.Identity.BankAccountIBAN"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Alias</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800" @onclick="SaveAlias">Save Alias</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (IsSaving)
|
||||
{
|
||||
<p>Saving...</p>
|
||||
}
|
||||
@if (IsSaving)
|
||||
{
|
||||
<p>Saving...</p>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -152,7 +157,7 @@ else
|
||||
|
||||
private bool EditMode { get; set; }
|
||||
private bool Loading { get; set; } = true;
|
||||
private Alias Obj { get; set; } = new();
|
||||
private AliasEdit Obj { get; set; } = new();
|
||||
private bool IsIdentityLoading { get; set; }
|
||||
private bool IsSaving { get; set; }
|
||||
|
||||
@@ -211,15 +216,17 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
Obj = alias;
|
||||
Obj = AliasToAliasEdit(alias);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new Obj
|
||||
Obj = new Alias();
|
||||
Obj.Identity = new Shared.Models.WebApi.Identity();
|
||||
Obj.Service = new Shared.Models.WebApi.Service();
|
||||
Obj.Password = new Shared.Models.WebApi.Password();
|
||||
var alias = new Alias();
|
||||
alias.Identity = new Shared.Models.WebApi.Identity();
|
||||
alias.Service = new Shared.Models.WebApi.Service();
|
||||
alias.Password = new Shared.Models.WebApi.Password();
|
||||
|
||||
Obj = AliasToAliasEdit(alias);
|
||||
}
|
||||
|
||||
// Hide loading spinner
|
||||
@@ -248,7 +255,7 @@ else
|
||||
Obj.Identity.AddressState = identity.Address.State;
|
||||
Obj.Identity.AddressZipCode = identity.Address.ZipCode;
|
||||
Obj.Identity.AddressCountry = identity.Address.Country;
|
||||
Obj.Identity.Hobbies = identity.Hobbies.First();
|
||||
Obj.Identity.Hobbies = identity.Hobbies[0];
|
||||
Obj.Identity.EmailPrefix = identity.EmailPrefix;
|
||||
Obj.Identity.PhoneMobile = identity.PhoneMobile;
|
||||
Obj.Identity.BankAccountIBAN = identity.BankAccountIBAN;
|
||||
@@ -285,7 +292,7 @@ else
|
||||
{
|
||||
try
|
||||
{
|
||||
DateTime.Parse(Obj.Identity.BirthDate);
|
||||
DateTime.Parse(Obj.Identity.BirthDate, new CultureInfo("en-US"));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -297,12 +304,12 @@ else
|
||||
{
|
||||
if (Id is not null)
|
||||
{
|
||||
Id = await AliasService.UpdateAliasAsync(Obj, Id.Value);
|
||||
Id = await AliasService.UpdateAliasAsync(AliasEditToAlias(Obj), Id.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Id = await AliasService.InsertAliasAsync(Obj);
|
||||
Id = await AliasService.InsertAliasAsync(AliasEditToAlias(Obj));
|
||||
}
|
||||
|
||||
IsSaving = false;
|
||||
@@ -327,4 +334,33 @@ else
|
||||
|
||||
Navigation.NavigateTo("/alias/" + Id);
|
||||
}
|
||||
|
||||
private AliasEdit AliasToAliasEdit(Alias alias)
|
||||
{
|
||||
return new AliasEdit
|
||||
{
|
||||
ServiceName = alias.Service.Name,
|
||||
ServiceUrl = alias.Service.Url,
|
||||
Password = alias.Password,
|
||||
Identity = alias.Identity,
|
||||
CreateDate = alias.CreateDate,
|
||||
LastUpdate = alias.LastUpdate
|
||||
};
|
||||
}
|
||||
|
||||
private Alias AliasEditToAlias(AliasEdit alias)
|
||||
{
|
||||
return new Alias
|
||||
{
|
||||
Service = new Service
|
||||
{
|
||||
Name = alias.ServiceName,
|
||||
Url = alias.ServiceUrl
|
||||
},
|
||||
Password = alias.Password,
|
||||
Identity = alias.Identity,
|
||||
CreateDate = alias.CreateDate,
|
||||
LastUpdate = alias.LastUpdate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@ public class EmailApiModel
|
||||
/// <summary>
|
||||
/// Gets or sets the list of attachments in the email.
|
||||
/// </summary>
|
||||
public List<AttachmentApiModel> Attachments { get; set; } = new();
|
||||
public List<AttachmentApiModel> Attachments { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ public class MailboxApiModel
|
||||
/// <summary>
|
||||
/// Gets or sets the list of mailbox email API models.
|
||||
/// </summary>
|
||||
public List<MailboxEmailApiModel> Mails { get; set; } = new();
|
||||
public List<MailboxEmailApiModel> Mails { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
AliasEmail = Alias?.Identity.EmailPrefix + "@landmail.nl";
|
||||
AliasEmail = Alias.Identity.EmailPrefix + "@landmail.nl";
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
@@ -49,7 +49,7 @@ public class PageBase : OwningComponentBase
|
||||
/// <summary>
|
||||
/// Gets or sets 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>();
|
||||
protected List<BreadcrumbItem> BreadcrumbItems { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the component asynchronously.
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private List<Shared.Models.WebApi.AliasListEntry> Aliases { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"http-release": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5067",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Release"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
|
||||
@@ -22,8 +22,8 @@ public class AliasService(HttpClient httpClient)
|
||||
/// <returns>Identity object.</returns>
|
||||
public async Task<Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
var identity = await httpClient.GetFromJsonAsync<Identity>("api/Identity/generate");
|
||||
if (identity == null)
|
||||
var identity = await httpClient.GetFromJsonAsync<Identity>("api/v1/Identity/generate");
|
||||
if (identity is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to generate random identity.");
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class AliasService(HttpClient httpClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
var returnObject = await httpClient.PutAsJsonAsync<Alias>("api/Alias", aliasObject);
|
||||
var returnObject = await httpClient.PutAsJsonAsync<Alias>("api/v1/Alias", aliasObject);
|
||||
return await returnObject.Content.ReadFromJsonAsync<Guid>();
|
||||
}
|
||||
catch
|
||||
@@ -59,7 +59,7 @@ public class AliasService(HttpClient httpClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
var returnObject = await httpClient.PostAsJsonAsync<Alias>("api/Alias/" + id, aliasObject);
|
||||
var returnObject = await httpClient.PostAsJsonAsync<Alias>("api/v1/Alias/" + id, aliasObject);
|
||||
return await returnObject.Content.ReadFromJsonAsync<Guid>();
|
||||
}
|
||||
catch
|
||||
@@ -77,7 +77,7 @@ public class AliasService(HttpClient httpClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetFromJsonAsync<Alias>("api/Alias/" + aliasId);
|
||||
return await httpClient.GetFromJsonAsync<Alias>("api/v1/Alias/" + aliasId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public class AliasService(HttpClient httpClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/Alias/items");
|
||||
return await httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/v1/Alias/items");
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -111,7 +111,7 @@ public class AliasService(HttpClient httpClient)
|
||||
// Delete from webapi.
|
||||
try
|
||||
{
|
||||
await httpClient.DeleteAsync("api/Alias/" + id);
|
||||
await httpClient.DeleteAsync("api/v1/Alias/" + id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -90,5 +90,14 @@ public class GlobalNotificationService
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all messages.
|
||||
/// </summary>
|
||||
public void ClearMessages()
|
||||
{
|
||||
SuccessMessages.Clear();
|
||||
ErrorMessages.Clear();
|
||||
}
|
||||
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
}
|
||||
|
||||
@@ -603,6 +603,14 @@ video {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.top-12 {
|
||||
top: 3rem;
|
||||
}
|
||||
|
||||
.top-10 {
|
||||
top: 2.5rem;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault.WebApp</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="css/tailwind.css" rel="stylesheet">
|
||||
<link href="AliasVault.WebApp.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-800">
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="js/dark-mode.js"></script>
|
||||
|
||||
<script>
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
window.clipboardCopy = {
|
||||
copyText: function (text) {
|
||||
navigator.clipboard.writeText(text).then(function () { })
|
||||
.catch(function (error) {
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
85
src/AliasVault.WebApp/wwwroot/index.template.html
Normal file
85
src/AliasVault.WebApp/wwwroot/index.template.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!--
|
||||
==========================================================================================================
|
||||
|
||||
db 88 88 8b d8 88
|
||||
d88b 88 "" `8b d8' 88 ,d
|
||||
d8'`8b 88 `8b d8' 88 88
|
||||
d8' `8b 88 88 ,adPPYYba, ,adPPYba, `8b d8' ,adPPYYba, 88 88 88 MM88MMM
|
||||
d8YaaaaY8b 88 88 "" `Y8 I8[ "" `8b d8' "" `Y8 88 88 88 88
|
||||
d8""""""""8b 88 88 ,adPPPPP88 `"Y8ba, `8b d8' ,adPPPPP88 88 88 88 88
|
||||
d8' `8b 88 88 88, ,88 aa ]8I `888' 88, ,88 "8a, ,a88 88 88,
|
||||
d8' `8b 88 88 `"8bbdP"Y8 `"YbbdP"' `8' `"8bbdP"Y8 `"YbbdP'Y8 88 "Y888
|
||||
|
||||
==========================================================================================================
|
||||
|
||||
AliasVault - Free open-source password manager.
|
||||
Build (UTC): @BuildVersion
|
||||
|
||||
Source code: https://github.com/lanedirt/AliasVault
|
||||
License: MIT
|
||||
-->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault.WebApp</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/app.css?v=@CacheBuster" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="css/tailwind.css?v=@CacheBuster" rel="stylesheet">
|
||||
<link href="AliasVault.WebApp.styles.css?v=@CacheBuster" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-800">
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="js/dark-mode.js?v=@CacheBuster"></script>
|
||||
|
||||
<script>
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
window.registerClickOutsideHandler = (dotNetHelper) => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const menu = document.getElementById('userMenuDropdown');
|
||||
const menuButton = document.getElementById('userMenuDropdownButton');
|
||||
if (menu && !menu.contains(event.target) && !menuButton.contains(event.target)) {
|
||||
dotNetHelper.invokeMethodAsync('CloseMenu');
|
||||
}
|
||||
|
||||
const mobileMenu = document.getElementById('mobileMenu');
|
||||
const mobileMenuButton = document.getElementById('toggleMobileMenuButton');
|
||||
if (mobileMenu && !mobileMenu.contains(event.target) && !mobileMenuButton.contains(event.target)) {
|
||||
dotNetHelper.invokeMethodAsync('CloseMenu');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.clipboardCopy = {
|
||||
copyText: function (text) {
|
||||
navigator.clipboard.writeText(text).then(function () { })
|
||||
.catch(function (error) {
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js?v=@CacheBuster"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,4 @@
|
||||
function initDarkModeSwitcher() {
|
||||
console.log('initDarkModeSwitcher');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
@@ -16,6 +15,11 @@ function initDarkModeSwitcher() {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to light mode if not set.
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
@@ -35,16 +39,13 @@ function initDarkModeSwitcher() {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
}
|
||||
|
||||
// if NOT set via local storage previously
|
||||
// if NOT set via local storage previously
|
||||
} else if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.dispatchEvent(event);
|
||||
|
||||
@@ -77,7 +77,7 @@ public class BlazorWasmAppManager
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBaseDirectory()
|
||||
private static string GetBaseDirectory()
|
||||
{
|
||||
string currentDir = Directory.GetCurrentDirectory();
|
||||
string baseDir = string.Empty;
|
||||
@@ -90,35 +90,8 @@ public class BlazorWasmAppManager
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
private async Task WaitForStartupAsync(int port)
|
||||
{
|
||||
// Wait for the application to start up
|
||||
var started = false;
|
||||
while (!started)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var response = await client.GetAsync($"http://localhost:{port}");
|
||||
started = response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (_blazorWasmErrors.Count > 0)
|
||||
{
|
||||
Assert.Fail($"WASM failed to start: {string.Join(Environment.NewLine, _blazorWasmErrors)}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if WINDOWS
|
||||
private void KillProcessAndChildrenWindows(int pid)
|
||||
private static void KillProcessAndChildrenWindows(int pid)
|
||||
{
|
||||
var searcher = new ManagementObjectSearcher($"Select * From Win32_Process Where ParentProcessID={pid}");
|
||||
var managementObjects = searcher.Get();
|
||||
@@ -140,7 +113,7 @@ public class BlazorWasmAppManager
|
||||
}
|
||||
}
|
||||
#else
|
||||
private void KillProcessAndChildrenUnix(int pid)
|
||||
private static void KillProcessAndChildrenUnix(int pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -178,4 +151,31 @@ public class BlazorWasmAppManager
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private async Task WaitForStartupAsync(int port)
|
||||
{
|
||||
// Wait for the application to start up
|
||||
var started = false;
|
||||
while (!started)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var response = await client.GetAsync($"http://localhost:{port}");
|
||||
started = response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await TestContext.Out.WriteLineAsync(e.Message);
|
||||
|
||||
if (_blazorWasmErrors.Count > 0)
|
||||
{
|
||||
Assert.Fail($"WASM failed to start: {string.Join(Environment.NewLine, _blazorWasmErrors)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ public class WebApplicationFactoryFixture<TEntryPoint> : WebApplicationFactory<T
|
||||
{
|
||||
builder.UseUrls(HostUrl);
|
||||
|
||||
// Set the JWT key environment variable to debug value.
|
||||
Environment.SetEnvironmentVariable("JWT_KEY", "12345678901234567890123456789012");
|
||||
|
||||
builder.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Remove the existing AliasDbContext registration.
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.1.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<DocumentationFile>bin\Release\net8.0\Cryptography.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user