Merge pull request #12 from lanedirt/11-implement-stylecop-analyzer-and-refactor

Code style refactor
This commit is contained in:
Leendert de Borst
2024-06-16 13:22:10 -07:00
committed by GitHub
90 changed files with 2815 additions and 874 deletions

View File

@@ -12,6 +12,28 @@ insert_final_newline = true
[*.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
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
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
# Razor files
[*.razor]
@@ -47,3 +69,56 @@ indent_size = 4
[*.xml]
indent_style = space
indent_size = 4
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
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

1
.gitignore vendored
View File

@@ -369,3 +369,4 @@ MigrationBackup/
FodyWeavers.xsd
.idea
*.licenseheader

View File

@@ -6,6 +6,18 @@ Note: all instructions below are based on MacOS. If you are using a different op
## Getting Started
In order to contribute to this project, you will need to have the following tools installed on your machine:
- Make sure to install the latest version of .NET SDK 8:
```bash
# Install .NET SDK 8
# On MacOS via brew:
brew install --cask dotnet-sdk
# On Windows via winget
winget install Microsoft.DotNet.SDK.8
```
- Dotnet CLI EF Tools
```bash

13
SonarLint.xml Normal file
View File

@@ -0,0 +1,13 @@
<SonarLint>
<Rules>
<Rule>
<Key>S1135</Key>
<Parameters>
<Parameter>
<Name>sonarlint.rule.enabled</Name>
<Value>false</Value>
</Parameter>
</Parameters>
</Rule>
</Rules>
</SonarLint>

View File

@@ -1,28 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault", "src\AliasVault\AliasVault.csproj", "{BD2050C0-DC26-4777-9514-546525307370}"
# Visual Studio Version 17
VisualStudioVersion = 17.10.34928.147
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault", "src\AliasVault\AliasVault.csproj", "{BD2050C0-DC26-4777-9514-546525307370}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasDb", "src\AliasDb\AliasDb.csproj", "{64F47C9A-FE69-4793-B469-28BAADEC6706}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasDb", "src\AliasDb\AliasDb.csproj", "{64F47C9A-FE69-4793-B469-28BAADEC6706}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasGenerators", "src\AliasGenerators\AliasGenerators.csproj", "{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasGenerators", "src\AliasGenerators\AliasGenerators.csproj", "{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaviconExtractor", "src\Utilities\FaviconExtractor\FaviconExtractor.csproj", "{ED328644-A152-403D-86EB-81201AA07744}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FaviconExtractor", "src\Utilities\FaviconExtractor\FaviconExtractor.csproj", "{ED328644-A152-403D-86EB-81201AA07744}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.UnitTests", "src\Tests\AliasVault.UnitTests\AliasVault.UnitTests.csproj", "{8E6A418A-B305-465D-857D-49953605C18E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.UnitTests", "src\Tests\AliasVault.UnitTests\AliasVault.UnitTests.csproj", "{8E6A418A-B305-465D-857D-49953605C18E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "src\Utilities\Cryptography\Cryptography.csproj", "{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "src\Utilities\Cryptography\Cryptography.csproj", "{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Api", "src\AliasVault.Api\AliasVault.Api.csproj", "{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Api", "src\AliasVault.Api\AliasVault.Api.csproj", "{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.WebApp", "src\AliasVault.WebApp\AliasVault.WebApp.csproj", "{25248E01-5A4B-4F95-A63C-BEA01499A1C2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.WebApp", "src\AliasVault.WebApp\AliasVault.WebApp.csproj", "{25248E01-5A4B-4F95-A63C-BEA01499A1C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Shared", "src\AliasVault.Shared\AliasVault.Shared.csproj", "{15EFE0D0-F41B-47D7-86B7-8F840335CB82}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Shared", "src\AliasVault.Shared\AliasVault.Shared.csproj", "{15EFE0D0-F41B-47D7-86B7-8F840335CB82}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.E2ETests", "src\Tests\AliasVault.E2ETests\AliasVault.E2ETests.csproj", "{AF013D08-1BF6-4E23-87D2-37F614BE7952}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests", "src\Tests\AliasVault.E2ETests\AliasVault.E2ETests.csproj", "{AF013D08-1BF6-4E23-87D2-37F614BE7952}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -71,10 +74,13 @@ Global
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{ED328644-A152-403D-86EB-81201AA07744} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
{AF013D08-1BF6-4E23-87D2-37F614BE7952} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
EndGlobalSection
EndGlobal

View File

@@ -4,22 +4,31 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -29,6 +38,7 @@
<Content Include="..\..\LICENSE.md">
<Link>LICENSE.md</Link>
</Content>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,15 @@
namespace AliasDb;
//-----------------------------------------------------------------------
// <copyright file="AliasDbContext.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 AliasDb;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
/// <summary>
/// The AliasDbContext class.
@@ -48,29 +55,7 @@ public class AliasDbContext : IdentityDbContext
/// <summary>
/// Gets or sets the AspNetUserRefreshTokens DbSet.
/// </summary>
public DbSet<AspNetUserRefreshTokens> AspNetUserRefreshTokens { get; set; }
/// <summary>
/// Sets up the connection string if it is not already configured.
/// </summary>
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// If the options are not already configured, use the appsettings.json file.
// Tip: the E2E tests will provide a different connection string directly
// which will override this default config.
/*if (!optionsBuilder.IsConfigured)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
optionsBuilder
.UseSqlite(configuration.GetConnectionString("AliasDbContext"))
.UseLazyLoadingProxies();
} */
}
public DbSet<AspNetUserRefreshToken> AspNetUserRefreshTokens { get; set; }
/// <summary>
/// The OnModelCreating method.
@@ -128,11 +113,31 @@ public class AliasDbContext : IdentityDbContext
.HasForeignKey(i => i.DefaultPasswordId)
.OnDelete(DeleteBehavior.SetNull);
// Configure the Login - UserId entity
modelBuilder.Entity<AspNetUserRefreshTokens>()
// Configure the User - AspNetUserRefreshToken entity
modelBuilder.Entity<AspNetUserRefreshToken>()
.HasOne(p => p.User)
.WithMany()
.HasForeignKey(p => p.UserId)
.IsRequired();
}
/// <summary>
/// Sets up the connection string if it is not already configured.
/// </summary>
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// If the options are not already configured, use the appsettings.json file.
if (!optionsBuilder.IsConfigured)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
optionsBuilder
.UseSqlite(configuration.GetConnectionString("AliasDbContext"))
.UseLazyLoadingProxies();
}
}
}

View File

@@ -1,13 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="AspNetUserRefreshToken.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 AliasDb;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
/// <summary>
/// Refresh tokens for users.
/// </summary>
public class AspNetUserRefreshTokens
public class AspNetUserRefreshToken
{
/// <summary>
/// Gets or sets Refresh Token ID.

View File

@@ -1,3 +1,9 @@
//-----------------------------------------------------------------------
// <copyright file="Identity.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 AliasDb;
using System.ComponentModel.DataAnnotations;
@@ -26,14 +32,14 @@ public class Identity
/// </summary>
[StringLength(255)]
[Column(TypeName = "VARCHAR")]
public string FirstName { get; set; } = null!;
public string? FirstName { get; set; } = null!;
/// <summary>
/// Gets or sets the last name.
/// </summary>
[StringLength(255)]
[Column(TypeName = "VARCHAR")]
public string LastName { get; set; } = null!;
public string? LastName { get; set; } = null!;
/// <summary>
/// Gets or sets the nickname.

View File

@@ -1,8 +1,14 @@
//-----------------------------------------------------------------------
// <copyright file="Login.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 AliasDb;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
/// <summary>
/// Login object.

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated/>
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated/>
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
// <auto-generated/>
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -20,7 +21,7 @@ namespace AliasDb.Migrations
// Fetch the first UserId from the AspNetUsers table and update the Logins table
migrationBuilder.Sql(@"
UPDATE Logins
UPDATE Logins
SET UserId = (SELECT Id FROM AspNetUsers LIMIT 1)
WHERE UserId = ''
");

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
// <auto-generated/>
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated/>
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -0,0 +1,540 @@
// <auto-generated />
using System;
using AliasDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasDb.Migrations
{
[DbContext(typeof(AliasDbContext))]
[Migration("20240616200303_ChangeColumnDefinitions")]
partial class ChangeColumnDefinitions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasDb.AspNetUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserRefreshTokens");
});
modelBuilder.Entity("AliasDb.Identity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AddressCity")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressCountry")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressState")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressStreet")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressZipCode")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("BankAccountIBAN")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("BirthDate")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid?>("DefaultPasswordId")
.HasColumnType("TEXT");
b.Property<string>("EmailPrefix")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("Gender")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("Hobbies")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("NickName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("PhoneMobile")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DefaultPasswordId");
b.ToTable("Identities");
});
modelBuilder.Entity("AliasDb.Login", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<Guid>("IdentityId")
.HasColumnType("TEXT");
b.Property<Guid>("ServiceId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("IdentityId");
b.HasIndex("ServiceId");
b.HasIndex("UserId");
b.ToTable("Logins");
});
modelBuilder.Entity("AliasDb.Password", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("LoginId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LoginId");
b.ToTable("Passwords");
});
modelBuilder.Entity("AliasDb.Service", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<byte[]>("Logo")
.HasColumnType("BLOB");
b.Property<string>("Name")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Url")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Services");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("AliasDb.AspNetUserRefreshToken", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasDb.Identity", b =>
{
b.HasOne("AliasDb.Password", "DefaultPassword")
.WithMany()
.HasForeignKey("DefaultPasswordId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DefaultPassword");
});
modelBuilder.Entity("AliasDb.Login", b =>
{
b.HasOne("AliasDb.Identity", "Identity")
.WithMany()
.HasForeignKey("IdentityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AliasDb.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Identity");
b.Navigation("Service");
b.Navigation("User");
});
modelBuilder.Entity("AliasDb.Password", b =>
{
b.HasOne("AliasDb.Login", "Login")
.WithMany("Passwords")
.HasForeignKey("LoginId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Login");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("AliasDb.Login", b =>
{
b.Navigation("Passwords");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasDb.Migrations
{
/// <inheritdoc />
public partial class ChangeColumnDefinitions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Identities",
type: "VARCHAR",
maxLength: 255,
nullable: true,
oldClrType: typeof(string),
oldType: "VARCHAR",
oldMaxLength: 255);
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Identities",
type: "VARCHAR",
maxLength: 255,
nullable: true,
oldClrType: typeof(string),
oldType: "VARCHAR",
oldMaxLength: 255);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Identities",
type: "VARCHAR",
maxLength: 255,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "VARCHAR",
oldMaxLength: 255,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Identities",
type: "VARCHAR",
maxLength: 255,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "VARCHAR",
oldMaxLength: 255,
oldNullable: true);
}
}
}

View File

@@ -16,12 +16,12 @@ namespace AliasDb.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.5")
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasDb.AspNetUserRefreshTokens", b =>
modelBuilder.Entity("AliasDb.AspNetUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -100,7 +100,6 @@ namespace AliasDb.Migrations
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("VARCHAR");
@@ -113,7 +112,6 @@ namespace AliasDb.Migrations
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("VARCHAR");
@@ -419,7 +417,7 @@ namespace AliasDb.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("AliasDb.AspNetUserRefreshTokens", b =>
modelBuilder.Entity("AliasDb.AspNetUserRefreshToken", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()

View File

@@ -1,3 +1,9 @@
//-----------------------------------------------------------------------
// <copyright file="Password.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 AliasDb;
using System.ComponentModel.DataAnnotations;

View File

@@ -1,3 +1,9 @@
//-----------------------------------------------------------------------
// <copyright file="Service.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 AliasDb;
using System.ComponentModel.DataAnnotations;

View File

@@ -4,10 +4,27 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,6 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="IIdentityGenerator.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 AliasGenerators.Identity;
/// <summary>
/// IdentityGenerator interface.
/// </summary>
public interface IIdentityGenerator
{
/// <summary>
/// Generates a random identity.
/// </summary>
/// <returns>Identity model object which contains the random identity.</returns>
Task<Models.Identity> GenerateRandomIdentityAsync();
}

View File

@@ -1,25 +1,38 @@
using System.Text.Json;
//-----------------------------------------------------------------------
// <copyright file="FigIdentityGenerator.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 AliasGenerators.Identity.Implementations;
using System.Text.Json;
/// <summary>
/// Identity generator which generates random identities using the identiteitgenerator.nl semi-public API.
/// </summary>
public class FigIdentityGenerator : IIdentityGenerator
{
private static readonly HttpClient httpClient = new HttpClient();
private static readonly HttpClient HttpClient = new();
private static readonly string Url = "https://api.identiteitgenerator.nl/generate/identity";
/// <inheritdoc/>
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
{
var response = await httpClient.GetAsync("https://api.identiteitgenerator.nl/generate/identity");
var response = await HttpClient.GetAsync(Url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
PropertyNameCaseInsensitive = true,
});
if (identity is null)
{
throw new InvalidOperationException("Failed to deserialize the identity from FIG WebApi.");
}
return identity;
}
}

View File

@@ -1,17 +1,23 @@
using AliasGenerators.Identity;
//-----------------------------------------------------------------------
// <copyright file="StaticIdentityGenerator.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 AliasGenerators.Identity.Implementations;
using AliasGenerators.Identity;
/// <summary>
/// Static identity generator which implements IIdentityGenerator but always returns
/// the same static identity for testing purposes.
/// </summary>
public class StaticIdentityGenerator : IIdentityGenerator
{
/// <inheritdoc/>
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
{
await Task.Delay(1); // Simulate async operation
await Task.Yield(); // Add an await statement to make the method truly asynchronous.
return new Identity.Models.Identity
{
FirstName = "John",

View File

@@ -0,0 +1,38 @@
//-----------------------------------------------------------------------
// <copyright file="Address.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 AliasGenerators.Identity.Models;
/// <summary>
/// Address model.
/// </summary>
public class Address
{
/// <summary>
/// Gets or sets the street.
/// </summary>
public string Street { get; set; } = null!;
/// <summary>
/// Gets or sets the city.
/// </summary>
public string City { get; set; } = null!;
/// <summary>
/// Gets or sets the state.
/// </summary>
public string State { get; set; } = null!;
/// <summary>
/// Gets or sets the zip code.
/// </summary>
public string ZipCode { get; set; } = null!;
/// <summary>
/// Gets or sets the country.
/// </summary>
public string Country { get; set; } = null!;
}

View File

@@ -1,38 +1,88 @@
//-----------------------------------------------------------------------
// <copyright file="Identity.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 AliasGenerators.Identity.Models;
/// <summary>
/// Identity model.
/// </summary>
public class Identity
{
public string Id { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>
public string Id { get; set; } = null!;
/// <summary>
/// Gets or sets the gender.
/// </summary>
public int Gender { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string NickName { get; set; }
/// <summary>
/// Gets or sets the first name.
/// </summary>
public string FirstName { get; set; } = null!;
/// <summary>
/// Gets or sets the last name.
/// </summary>
public string LastName { get; set; } = null!;
/// <summary>
/// Gets or sets the nickname. This is also used as the username.
/// </summary>
public string NickName { get; set; } = null!;
/// <summary>
/// Gets or sets the birth date.
/// </summary>
public DateTime BirthDate { get; set; }
public Address Address { get; set; }
public Job Job { get; set; }
public List<string> Hobbies { get; set; }
public string EmailPrefix { get; set; }
public string Password { get; set; }
public string PhoneMobile { get; set; }
public string BankAccountIBAN { get; set; }
public string ProfilePhotoBase64 { get; set; }
public string ProfilePhotoPrompt { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
/// <summary>
/// Gets or sets the address.
/// </summary>
public Address Address { get; set; } = null!;
public class Job
{
public string Title { get; set; }
public string Company { get; set; }
public string Salary { get; set; }
public decimal SalaryCalculated { get; set; }
public string Description { get; set; }
/// <summary>
/// Gets or sets the job.
/// </summary>
public Job Job { get; set; } = null!;
/// <summary>
/// Gets or sets the hobbies.
/// </summary>
public List<string> Hobbies { get; set; } = null!;
/// <summary>
/// Gets or sets the email address prefix.
/// </summary>
public string EmailPrefix { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; } = null!;
/// <summary>
/// Gets or sets the phone mobile.
/// </summary>
public string PhoneMobile { get; set; } = null!;
/// <summary>
/// Gets or sets the bank account IBAN.
/// </summary>
public string BankAccountIBAN { get; set; } = null!;
/// <summary>
/// Gets or sets the profile photo in base64 format.
/// </summary>
public string ProfilePhotoBase64 { get; set; } = null!;
/// <summary>
/// Gets or sets the profile photo prompt.
/// </summary>
public string ProfilePhotoPrompt { get; set; } = null!;
}

View File

@@ -0,0 +1,38 @@
//-----------------------------------------------------------------------
// <copyright file="Job.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 AliasGenerators.Identity.Models;
/// <summary>
/// Job model.
/// </summary>
public class Job
{
/// <summary>
/// Gets or sets the title.
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// Gets or sets the company.
/// </summary>
public string Company { get; set; } = null!;
/// <summary>
/// Gets or sets the salary.
/// </summary>
public string Salary { get; set; } = null!;
/// <summary>
/// Gets or sets the calculated salary.
/// </summary>
public decimal SalaryCalculated { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
public string Description { get; set; } = null!;
}

View File

@@ -1,6 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="IPasswordGenerator.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 AliasGenerators.Implementations;
/// <summary>
/// Interface for password generators.
/// </summary>
public interface IPasswordGenerator
{
/// <summary>
/// Generates a random password.
/// </summary>
/// <returns>Random generated password as string.</returns>
string GenerateRandomPassword();
}

View File

@@ -1,3 +1,9 @@
//-----------------------------------------------------------------------
// <copyright file="SpamOkPasswordGenerator.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 AliasGenerators.Password.Implementations;
using AliasGenerators.Implementations;
@@ -7,10 +13,7 @@ using AliasGenerators.Implementations;
/// </summary>
public class SpamOkPasswordGenerator : IPasswordGenerator
{
/// <summary>
/// Generates a random password using the SpamOK library with diceware (dictionary) method.
/// </summary>
/// <returns></returns>
/// <inheritdoc/>
public string GenerateRandomPassword()
{
var passwordBuilder = new SpamOK.PasswordGenerator.BasicPasswordBuilder();
@@ -24,7 +27,6 @@ public class SpamOkPasswordGenerator : IPasswordGenerator
.GeneratePassword()
.ToString();
return password;
}
}

View File

@@ -6,6 +6,15 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AliasVault.Api</RootNamespace>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -16,7 +25,11 @@
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.6.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
@@ -29,6 +42,7 @@
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

View File

@@ -1,22 +1,32 @@
//-----------------------------------------------------------------------
// <copyright file="AliasController.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.Api.Controllers;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using AliasDb;
using AliasVault.Shared.Models.WebApi;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Identity = AliasVault.Shared.Models.WebApi.Identity;
using Service = AliasVault.Shared.Models.WebApi.Service;
public class AliasController : AuthenticatedRequestController
/// <summary>
/// Alias controller for handling CRUD operations on the database for alias entities.
/// </summary>
/// <param name="context">DbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
public class AliasController(AliasDbContext context, UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
{
private readonly AliasDbContext _context;
public AliasController(AliasDbContext context, UserManager<IdentityUser> userManager) : base(userManager)
{
_context = context;
}
/// <summary>
/// Get all alias items for the current user.
/// </summary>
/// <returns>List of aliases in JSON format.</returns>
[HttpGet("items")]
public async Task<IActionResult> GetItems()
{
@@ -27,7 +37,7 @@ public class AliasController : AuthenticatedRequestController
}
// Logic to retrieve items for the user.
var aliases = await _context.Logins
var aliases = await context.Logins
.Include(x => x.Identity)
.Include(x => x.Service)
.Where(x => x.UserId == user.Id)
@@ -35,8 +45,8 @@ public class AliasController : AuthenticatedRequestController
{
Id = x.Id,
Logo = x.Service.Logo,
Service = x.Service.Name,
CreateDate = x.CreatedAt
Service = x.Service.Name ?? "n/a",
CreateDate = x.CreatedAt,
})
.ToListAsync();
@@ -44,6 +54,11 @@ public class AliasController : AuthenticatedRequestController
return Ok(aliases);
}
/// <summary>
/// Get a single alias item by its ID.
/// </summary>
/// <param name="aliasId">ID of the alias.</param>
/// <returns>Alias object as JSON.</returns>
[HttpGet("{aliasId}")]
public async Task<IActionResult> GetAlias(Guid aliasId)
{
@@ -53,7 +68,7 @@ public class AliasController : AuthenticatedRequestController
return Unauthorized();
}
var aliasObject = await _context.Logins
var aliasObject = await context.Logins
.Include(x => x.Passwords)
.Include(x => x.Identity)
.Include(x => x.Service)
@@ -63,11 +78,11 @@ public class AliasController : AuthenticatedRequestController
{
Service = new Service()
{
Name = x.Service.Name,
Name = x.Service.Name ?? "n/a",
Url = x.Service.Url,
LogoUrl = "",
LogoUrl = string.Empty,
CreatedAt = x.Service.CreatedAt,
UpdatedAt = x.Service.UpdatedAt
UpdatedAt = x.Service.UpdatedAt,
},
Identity = new Identity()
{
@@ -86,18 +101,17 @@ public class AliasController : AuthenticatedRequestController
PhoneMobile = x.Identity.PhoneMobile,
BankAccountIBAN = x.Identity.BankAccountIBAN,
CreatedAt = x.Identity.CreatedAt,
UpdatedAt = x.Identity.UpdatedAt
UpdatedAt = x.Identity.UpdatedAt,
},
Password = new AliasVault.Shared.Models.WebApi.Password()
{
Value = x.Passwords.First().Value ?? "",
Description = "",
Value = x.Passwords.First().Value ?? string.Empty,
Description = string.Empty,
CreatedAt = x.Passwords.First().CreatedAt,
UpdatedAt = x.Passwords.First().UpdatedAt
UpdatedAt = x.Passwords.First().UpdatedAt,
},
CreateDate = x.CreatedAt,
LastUpdate = x.UpdatedAt
LastUpdate = x.UpdatedAt,
})
.FirstAsync();
@@ -105,10 +119,10 @@ public class AliasController : AuthenticatedRequestController
}
/// <summary>
/// Insert a new entry to the database.
/// Insert a new alias to the database.
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
/// <param name="model">Alias model.</param>
/// <returns>ID of newly inserted alias.</returns>
[HttpPut("")]
public async Task<IActionResult> Insert([FromBody] Alias model)
{
@@ -118,35 +132,37 @@ public class AliasController : AuthenticatedRequestController
return Unauthorized();
}
var login = new Login();
login.UserId = user.Id;
login.CreatedAt = DateTime.UtcNow;
login.UpdatedAt = DateTime.UtcNow;
login.Identity = new AliasDb.Identity()
var login = new Login
{
NickName = model.Identity.NickName,
FirstName = model.Identity.FirstName,
LastName = model.Identity.LastName,
BirthDate = DateTime.Parse(model.Identity.BirthDate),
Gender = model.Identity.Gender,
AddressStreet = model.Identity.AddressStreet,
AddressCity = model.Identity.AddressCity,
AddressState = model.Identity.AddressState,
AddressZipCode = model.Identity.AddressZipCode,
AddressCountry = model.Identity.AddressCountry,
Hobbies = model.Identity.Hobbies,
EmailPrefix = model.Identity.EmailPrefix,
PhoneMobile = model.Identity.PhoneMobile,
BankAccountIBAN = model.Identity.BankAccountIBAN,
UserId = user.Id,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow,
Identity = new AliasDb.Identity()
{
NickName = model.Identity.NickName,
FirstName = model.Identity.FirstName,
LastName = model.Identity.LastName,
BirthDate = DateTime.Parse(model.Identity.BirthDate ?? "1900-01-01", new CultureInfo("en-US")),
Gender = model.Identity.Gender,
AddressStreet = model.Identity.AddressStreet,
AddressCity = model.Identity.AddressCity,
AddressState = model.Identity.AddressState,
AddressZipCode = model.Identity.AddressZipCode,
AddressCountry = model.Identity.AddressCountry,
Hobbies = model.Identity.Hobbies,
EmailPrefix = model.Identity.EmailPrefix,
PhoneMobile = model.Identity.PhoneMobile,
BankAccountIBAN = model.Identity.BankAccountIBAN,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
},
};
login.Passwords.Add(new AliasDb.Password()
{
Value = model.Password.Value,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow,
});
login.Service = new AliasDb.Service()
@@ -154,23 +170,23 @@ public class AliasController : AuthenticatedRequestController
Name = model.Service.Name,
Url = model.Service.Url,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow,
};
_context.Logins.Add(login);
await _context.SaveChangesAsync();
await context.Logins.AddAsync(login);
await context.SaveChangesAsync();
return Ok(login.Id);
}
/// <summary>
/// Update an existing entry in the database.
/// Update an existing alias entry in the database.
/// </summary>
/// <param name="model"></param>
/// <param name="aliasId"></param>
/// <returns></returns>
/// <param name="aliasId">The alias ID to update.</param>
/// <param name="model">Alias model.</param>
/// <returns>ID of updated alias entry.</returns>
[HttpPost("{aliasId}")]
public async Task<IActionResult> Update([FromBody] Alias model, Guid aliasId)
public async Task<IActionResult> Update(Guid aliasId, [FromBody] Alias model)
{
var user = await GetCurrentUserAsync();
if (user == null)
@@ -179,7 +195,7 @@ public class AliasController : AuthenticatedRequestController
}
// Get the existing entry.
var login = await _context.Logins
var login = await context.Logins
.Include(x => x.Identity)
.Include(x => x.Service)
.Include(x => x.Passwords)
@@ -191,7 +207,7 @@ public class AliasController : AuthenticatedRequestController
login.Identity.NickName = model.Identity.NickName;
login.Identity.FirstName = model.Identity.FirstName;
login.Identity.LastName = model.Identity.LastName;
login.Identity.BirthDate = DateTime.Parse(model.Identity.BirthDate);
login.Identity.BirthDate = DateTime.Parse(model.Identity.BirthDate ?? "1900-01-01", new CultureInfo("en-US"));
login.Identity.Gender = model.Identity.Gender;
login.Identity.AddressStreet = model.Identity.AddressStreet;
login.Identity.AddressCity = model.Identity.AddressCity;
@@ -210,15 +226,16 @@ public class AliasController : AuthenticatedRequestController
login.Service.Url = model.Service.Url;
login.Service.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
return Ok(login.Id);
}
/// <summary>
/// Delete an existing entry from the database.
/// Delete an existing alias entry from the database.
/// </summary>
/// <param name="aliasId"></param>
/// <param name="aliasId">ID of the alias to delete.</param>
/// <returns>HTTP status code.</returns>
[HttpDelete("{aliasId}")]
public async Task<IActionResult> Delete(Guid aliasId)
{
@@ -228,13 +245,13 @@ public class AliasController : AuthenticatedRequestController
return Unauthorized();
}
var login = await _context.Logins
var login = await context.Logins
.Where(x => x.Id == aliasId)
.Where(x => x.UserId == user.Id)
.FirstAsync();
_context.Logins.Remove(login);
await _context.SaveChangesAsync();
context.Logins.Remove(login);
await context.SaveChangesAsync();
return Ok();
}

View File

@@ -1,47 +1,56 @@
using System.Security.Cryptography;
using AliasDb;
using AliasVault.Shared.Models;
//-----------------------------------------------------------------------
// <copyright file="AuthController.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.Api.Controllers;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using AliasDb;
using AliasVault.Shared.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
/// <summary>
/// Auth controller for handling authentication.
/// </summary>
/// <param name="context">AliasDbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
/// <param name="signInManager">SignInManager instance.</param>
/// <param name="configuration">IConfiguration instance.</param>
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
public class AuthController(AliasDbContext context, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IConfiguration configuration) : ControllerBase
{
private readonly AliasDbContext _context;
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IConfiguration _configuration;
private const string LoginProvider = "AliasVault";
private const string RefreshToken = "RefreshToken";
public AuthController(AliasDbContext context, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IConfiguration configuration)
{
_context = context;
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
/// <summary>
/// Login endpoint used to process login attempt using credentials.
/// </summary>
/// <param name="model">Login model.</param>
/// <returns>IActionResult.</returns>
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
var user = await userManager.FindByEmailAsync(model.Email);
if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
{
var tokenModel = await GenerateNewTokenForUser(user);
return Ok(tokenModel);
}
return Unauthorized();
}
/// <summary>
/// Refresh endpoint used to refresh an expired access token using a valid refresh token.
/// </summary>
/// <param name="tokenModel">Token model.</param>
/// <returns>IActionResult.</returns>
[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] TokenModel tokenModel)
{
@@ -51,7 +60,7 @@ public class AuthController : ControllerBase
return Unauthorized("User not found (email-1)");
}
var user = await _userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "");
var user = await userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty);
if (user == null)
{
return Unauthorized("User not found (email-2)");
@@ -60,34 +69,38 @@ public class AuthController : ControllerBase
// Check if the refresh token is valid.
// Remove any existing refresh tokens for this user and device.
var deviceIdentifier = GenerateDeviceIdentifier(Request);
var existingToken = _context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier).FirstOrDefault();
var existingToken = context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier).FirstOrDefault();
if (existingToken == null || existingToken.Value != tokenModel.RefreshToken || existingToken.ExpireDate < DateTime.Now)
{
return Unauthorized("Refresh token expired");
}
// Remove the existing refresh token.
_context.AspNetUserRefreshTokens.Remove(existingToken);
context.AspNetUserRefreshTokens.Remove(existingToken);
// Generate a new refresh token to replace the old one.
var newRefreshToken = GenerateRefreshToken();
// Add new refresh token.
_context.AspNetUserRefreshTokens.Add(new AspNetUserRefreshTokens
await context.AspNetUserRefreshTokens.AddAsync(new AspNetUserRefreshToken
{
UserId = user.Id,
DeviceIdentifier = deviceIdentifier,
Value = newRefreshToken,
ExpireDate = DateTime.Now.AddDays(30),
CreatedAt = DateTime.Now
CreatedAt = DateTime.Now,
});
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
var token = GenerateJwtToken(user);
return Ok(new TokenModel() { Token = token, RefreshToken = newRefreshToken });
}
/// <summary>
/// Revoke endpoint used to revoke a refresh token.
/// </summary>
/// <param name="model">Token model.</param>
/// <returns>IActionResult.</returns>
[HttpPost("revoke")]
public async Task<IActionResult> Revoke([FromBody] TokenModel model)
{
@@ -97,7 +110,7 @@ public class AuthController : ControllerBase
return Unauthorized("User not found (email-1)");
}
var user = await _userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "");
var user = await userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty);
if (user == null)
{
return Unauthorized("User not found (email-2)");
@@ -105,29 +118,34 @@ public class AuthController : ControllerBase
// Check if the refresh token is valid.
var deviceIdentifier = GenerateDeviceIdentifier(Request);
var existingToken = _context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier).FirstOrDefault();
var existingToken = context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier).FirstOrDefault();
if (existingToken == null || existingToken.Value != model.RefreshToken)
{
return Unauthorized("Invalid refresh token");
}
// Remove the existing refresh token.
_context.AspNetUserRefreshTokens.Remove(existingToken);
await _context.SaveChangesAsync();
context.AspNetUserRefreshTokens.Remove(existingToken);
await context.SaveChangesAsync();
return Ok("Refresh token revoked successfully");
}
/// <summary>
/// Register endpoint used to register a new user.
/// </summary>
/// <param name="model">Register model.</param>
/// <returns>IActionResult.</returns>
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
var user = new IdentityUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
var result = await userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// When a user is registered, they are automatically signed in.
await _signInManager.SignInAsync(user, isPersistent: false);
await signInManager.SignInAsync(user, isPersistent: false);
// Return the token.
var tokenModel = await GenerateNewTokenForUser(user);
@@ -141,24 +159,23 @@ public class AuthController : ControllerBase
private string GenerateJwtToken(IdentityUser user)
{
var claims = new[]
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
new(ClaimTypes.NameIdentifier, user.Id ?? string.Empty),
new(ClaimTypes.Name, user.UserName ?? string.Empty),
new(ClaimTypes.Email, user.Email ?? string.Empty),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Issuer"],
issuer: configuration["Jwt:Issuer"] ?? string.Empty,
audience: configuration["Jwt:Issuer"] ?? string.Empty,
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds
);
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
@@ -166,11 +183,10 @@ public class AuthController : ControllerBase
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
@@ -180,14 +196,13 @@ public class AuthController : ControllerBase
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])),
ValidateLifetime = false
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty)),
ValidateLifetime = false,
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
@@ -198,8 +213,8 @@ public class AuthController : ControllerBase
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["User-Agent"].ToString();
var acceptLanguage = request.Headers["Accept-Language"].ToString();
var userAgent = request.Headers.UserAgent.ToString();
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
var rawIdentifier = $"{userAgent}|{acceptLanguage}";
return rawIdentifier;
@@ -215,19 +230,19 @@ public class AuthController : ControllerBase
// Save refresh token to database.
// Remove any existing refresh tokens for this user and device.
var existingTokens = _context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier);
_context.AspNetUserRefreshTokens.RemoveRange(existingTokens);
var existingTokens = context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier);
context.AspNetUserRefreshTokens.RemoveRange(existingTokens);
// Add new refresh token.
_context.AspNetUserRefreshTokens.Add(new AspNetUserRefreshTokens
await context.AspNetUserRefreshTokens.AddAsync(new AspNetUserRefreshToken
{
UserId = user.Id,
DeviceIdentifier = deviceIdentifier,
Value = refreshToken,
ExpireDate = DateTime.Now.AddDays(30),
CreatedAt = DateTime.Now
CreatedAt = DateTime.Now,
});
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
return new TokenModel() { Token = token, RefreshToken = refreshToken };
}

View File

@@ -1,26 +1,32 @@
using Microsoft.AspNetCore.Authorization;
//-----------------------------------------------------------------------
// <copyright file="AuthenticatedRequestController.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.Api.Controllers;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
/// <summary>
/// Base controller for requests that require authentication.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class AuthenticatedRequestController : ControllerBase
public class AuthenticatedRequestController(UserManager<IdentityUser> userManager) : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
public AuthenticatedRequestController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
/// <summary>
/// Get the current authenticated user.
/// </summary>
/// <returns>IdentityUser object for current user.</returns>
protected async Task<IdentityUser?> GetCurrentUserAsync()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return await _userManager.FindByIdAsync(userId);
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unable to find user ID.");
return await userManager.FindByIdAsync(userId);
}
}

View File

@@ -1,21 +1,26 @@
using AliasGenerators.Identity;
using AliasGenerators.Identity.Implementations;
//-----------------------------------------------------------------------
// <copyright file="IdentityController.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.Api.Controllers;
using AliasGenerators.Identity;
using AliasGenerators.Identity.Implementations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
public class IdentityController : AuthenticatedRequestController
/// <summary>
/// Controller for identity generation.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
public class IdentityController(UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
{
public IdentityController(UserManager<IdentityUser> userManager) : base(userManager)
{
}
/// <summary>
/// Proxies the request to the identity generator to generate a random identity.
/// </summary>
/// <returns></returns>
/// <returns>Identity model.</returns>
[HttpGet("generate")]
public async Task<IActionResult> Generate()
{

View File

@@ -1,3 +1,10 @@
//-----------------------------------------------------------------------
// <copyright file="Program.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>
//-----------------------------------------------------------------------
using System.Data.Common;
using System.Text;
using AliasDb;
@@ -14,9 +21,9 @@ var configuration = builder.Configuration;
builder.Services.AddLogging(logging =>
{
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Debug); // Set the minimum level to Information
logging.AddFilter("Microsoft.AspNetCore.Identity.DataProtectorTokenProvider", LogLevel.Debug); // Ensure Identity logs are captured
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Debug); // Ensure Identity logs are captured
logging.SetMinimumLevel(LogLevel.Error);
logging.AddFilter("Microsoft.AspNetCore.Identity.DataProtectorTokenProvider", LogLevel.Error);
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
});
// Add services to the container.
@@ -42,7 +49,7 @@ builder.Services.AddDbContext<AliasDbContext>((container, options) =>
builder.Services.AddDataProtection();
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromDays(30); // Set token lifespan for refresh tokens
options.TokenLifespan = TimeSpan.FromDays(30);
options.Name = "AliasVault";
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
@@ -57,8 +64,6 @@ builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
options.Tokens.ProviderMap.Add("AliasVault", new TokenProviderDescriptor(typeof(DataProtectorTokenProvider<IdentityUser>)));
})
.AddEntityFrameworkStores<AliasDbContext>()
// Note: The AliasVault token provider is used to generate refresh tokens and is also defined
// in the AuthController.
.AddDefaultTokenProviders()
.AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>("AliasVault");
@@ -78,7 +83,7 @@ builder.Services.AddAuthentication(options =>
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty)),
ClockSkew = TimeSpan.Zero,
};
});
@@ -86,7 +91,8 @@ builder.Services.AddAuthentication(options =>
// Configure CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
options.AddPolicy(
"CorsPolicy",
policy => policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
@@ -106,20 +112,22 @@ builder.Services.AddSwaggerGen(c =>
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
Scheme = "Bearer",
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
new OpenApiSecurityScheme
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer",
},
},
Array.Empty<string>()
},
Array.Empty<string>()
}});
});
});
var app = builder.Build();
@@ -144,25 +152,18 @@ using (var scope = app.Services.CreateScope())
var container = scope.ServiceProvider;
var db = container.GetRequiredService<AliasDbContext>();
db.Database.EnsureCreated();
/*if (!db..Any())
{
try
{
db.Initialize();
}
catch (Exception ex)
{
var logger = container.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the database. Error: {Message}", ex.Message);
}
}*/
await db.Database.EnsureCreatedAsync();
}
app.Run();
await app.RunAsync();
/// <summary>
/// For starting the WebAPI project in-memory from E2ETests project.
/// </summary>
public class AliasVaultApiProgram { }
namespace AliasVault.Api
{
/// <summary>
/// Explicit program class definition. This is required in order to start the WebAPI project
/// in-memory from E2ETests project via WebApplicationFactory.
/// </summary>
public partial class Program
{
}
}

View File

@@ -6,4 +6,25 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Shared.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Shared.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,7 +1,24 @@
//-----------------------------------------------------------------------
// <copyright file="LoginModel.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;
/// <summary>
/// Login model.
/// </summary>
public class LoginModel
{
public string Email { get; set; }
public string Password { get; set; }
/// <summary>
/// Gets or sets the email.
/// </summary>
public string Email { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; } = null!;
}

View File

@@ -1,9 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="RegisterModel.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;
/// <summary>
/// Register model.
/// </summary>
public class RegisterModel
{
public string Email { get; set; }
public string Password { get; set; }
public string PasswordConfirm { get; set; }
public bool AcceptTerms { get; set; }
/// <summary>
/// Gets or sets the email.
/// </summary>
public string Email { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
public string PasswordConfirm { get; set; } = null!;
/// <summary>
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
/// </summary>
public bool AcceptTerms { get; set; } = false;
}

View File

@@ -1,12 +1,28 @@
using System.Text.Json.Serialization;
//-----------------------------------------------------------------------
// <copyright file="TokenModel.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;
using System.Text.Json.Serialization;
/// <summary>
/// Token model.
/// </summary>
public class TokenModel
{
/// <summary>
/// Gets or sets the token.
/// </summary>
[JsonPropertyName("token")]
public string Token { get; set; }
public string Token { get; set; } = null!;
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
[JsonPropertyName("refreshToken")]
public string RefreshToken { get; set; }
public string RefreshToken { get; set; } = null!;
}

View File

@@ -1,10 +1,39 @@
//-----------------------------------------------------------------------
// <copyright file="Alias.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;
/// <summary>
/// Alias model.
/// </summary>
public class Alias
{
public Service Service { get; set; }
public Identity Identity { get; set; }
public Password Password { get; set; }
/// <summary>
/// Gets or sets the Alias Service object.
/// </summary>
public Service Service { get; set; } = null!;
/// <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; }
}

View File

@@ -1,9 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="AliasListEntry.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;
/// <summary>
/// Alias list entry model. This model is used to represent an alias in a list with simplified properties.
/// </summary>
public class AliasListEntry
{
/// <summary>
/// Gets or sets the alias id.
/// </summary>
public Guid Id { get; set; }
public byte[] Logo { get; set; }
public string Service { get; set; }
/// <summary>
/// Gets or sets the alias logo byte array.
/// </summary>
public byte[]? Logo { get; set; }
/// <summary>
/// Gets or sets the alias service name.
/// </summary>
public string Service { get; set; } = null!;
/// <summary>
/// Gets or sets the alias create date.
/// </summary>
public DateTime CreateDate { get; set; }
}

View File

@@ -1,23 +1,104 @@
//-----------------------------------------------------------------------
// <copyright file="Identity.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;
/// <summary>
/// Identity model.
/// </summary>
public class Identity
{
/// <summary>
/// Gets or sets the identity id.
/// </summary>
public Guid Id { get; set; }
public string Gender { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string NickName { get; set; }
public string BirthDate { get; set; }
public string AddressStreet { get; set; }
public string AddressCity { get; set; }
/// <summary>
/// Gets or sets the gender.
/// </summary>
public string? Gender { get; set; }
/// <summary>
/// Gets or sets the first name.
/// </summary>
public string? FirstName { get; set; }
/// <summary>
/// Gets or sets the last name.
/// </summary>
public string? LastName { get; set; }
/// <summary>
/// Gets or sets the nickname.
/// </summary>
public string? NickName { get; set; }
/// <summary>
/// Gets or sets the birth date.
/// </summary>
public string? BirthDate { get; set; }
/// <summary>
/// Gets or sets the street address.
/// </summary>
public string? AddressStreet { get; set; }
/// <summary>
/// Gets or sets the city.
/// </summary>
public string? AddressCity { get; set; }
/// <summary>
/// Gets or sets the state.
/// </summary>
public string? AddressState { get; set; }
public string AddressZipCode { get; set; }
public string AddressCountry { get; set; }
public string Hobbies { get; set; }
public string EmailPrefix { get; set; }
public string PhoneMobile { get; set; }
public string BankAccountIBAN { get; set; }
/// <summary>
/// Gets or sets the zip code.
/// </summary>
public string? AddressZipCode { get; set; }
/// <summary>
/// Gets or sets the country.
/// </summary>
public string? AddressCountry { get; set; }
/// <summary>
/// Gets or sets the hobbies.
/// </summary>
public string? Hobbies { get; set; }
/// <summary>
/// Gets or sets the email prefix.
/// </summary>
public string? EmailPrefix { get; set; }
/// <summary>
/// Gets or sets the mobile phone number.
/// </summary>
public string? PhoneMobile { get; set; }
/// <summary>
/// Gets or sets the bank account IBAN.
/// </summary>
public string? BankAccountIBAN { get; set; }
/// <summary>
/// Gets or sets the date and time of creation.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the date and time of last update.
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the default password.
/// </summary>
public Password? DefaultPassword { get; set; }
}

View File

@@ -1,9 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="Password.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;
/// <summary>
/// Password model.
/// </summary>
public class Password
{
public string Value { get; set; }
/// <summary>
/// Gets or sets the value of the password.
/// </summary>
public string Value { get; set; } = null!;
/// <summary>
/// Gets or sets the description of the password.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the date and time when the password was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the date and time when the password was last updated.
/// </summary>
public DateTime UpdatedAt { get; set; }
}

View File

@@ -1,11 +1,44 @@
//-----------------------------------------------------------------------
// <copyright file="Service.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;
/// <summary>
/// Service model.
/// </summary>
public class Service
{
public string Name { get; set; }
/// <summary>
/// Gets or sets the name of the service.
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Gets or sets the description of the service.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the URL of the service.
/// </summary>
public string? Url { get; set; }
/// <summary>
/// Gets or sets the logo URL of the service.
/// </summary>
public string? LogoUrl { get; set; }
/// <summary>
/// Gets or sets the creation date and time of the service.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the last updated date and time of the service.
/// </summary>
public DateTime UpdatedAt { get; set; }
}

View File

@@ -7,13 +7,36 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.WebApp.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<DocumentationFile>bin\Release\net8.0\AliasVault.WebApp.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.2" PrivateAssets="all"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -23,6 +46,7 @@
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,10 +9,33 @@
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
@code {
[Parameter] public string Id { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
[Parameter] public string Placeholder { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> AdditionalAttributes { get; set; }
/// <summary>
/// Gets or sets the ID of the input field.
/// </summary>
[Parameter] public string Id { get; set; } = null!;
/// <summary>
/// Gets or sets the value of the input field.
/// </summary>
[Parameter] public string Value { get; set; } = null!;
/// <summary>
/// Gets or sets the event callback that is triggered when the value changes.
/// </summary>
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
/// <summary>
/// Gets or sets the expression that identifies the value property.
/// </summary>
[Parameter] public Expression<Func<string>> ValueExpression { get; set; } = null!;
/// <summary>
/// Gets or sets the placeholder text for the input field.
/// </summary>
[Parameter] public string Placeholder { get; set; } = null!;
/// <summary>
/// Gets or sets additional attributes for the input field.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
}

View File

@@ -1,14 +1,15 @@
@page "/user/login"
@attribute [AllowAnonymous]
@layout Auth.Layout.MainLayout
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Services
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject AuthService AuthService
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Services
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
Sign in to AliasVault
@@ -45,7 +46,7 @@
@code {
LoginModel user = new LoginModel();
FullScreenLoadingIndicator loadingIndicator;
FullScreenLoadingIndicator loadingIndicator = new();
protected override async Task OnInitializedAsync()
{

View File

@@ -1,16 +1,15 @@
@page "/user/register"
@attribute [AllowAnonymous]
@layout Auth.Layout.MainLayout
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Services
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject ILocalStorageService LocalStorage
@inject NavigationManager NavigationManager
@inject AuthService AuthService
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Services
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
Create a Free Account
@@ -61,7 +60,7 @@
@code {
RegisterModel user = new RegisterModel();
FullScreenLoadingIndicator loadingIndicator;
FullScreenLoadingIndicator loadingIndicator = new();
List<string> validationErrors = new List<string>();
async Task HandleRegister()
@@ -114,10 +113,10 @@
public class ValidationErrorResponse
{
public string Type { get; set; }
public string Title { get; set; }
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public int Status { get; set; }
public IDictionary<string, string[]> Errors { get; set; }
public string TraceId { get; set; }
public Dictionary<string, string[]> Errors { get; set; } = new();
public string TraceId { get; set; } = null!;
}
}

View File

@@ -1,24 +1,48 @@
using AliasVault.WebApp.Auth.Services;
using Blazored.LocalStorage;
//-----------------------------------------------------------------------
// <copyright file="AuthStateProvider.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.WebApp;
namespace AliasVault.WebApp.Auth.Providers;
using System.Security.Claims;
using System.Text.Json;
using AliasVault.WebApp.Auth.Services;
using Microsoft.AspNetCore.Components.Authorization;
public class CustomAuthStateProvider : AuthenticationStateProvider
/// <summary>
/// Custom authentication state provider for the application.
/// </summary>
public class AuthStateProvider(AuthService authService) : AuthenticationStateProvider
{
private readonly AuthService _authService;
public CustomAuthStateProvider(AuthService authService)
/// <summary>
/// Parses the claims from the JWT token.
/// </summary>
/// <param name="jwt">The JWT token.</param>
/// <returns>The claims parsed from the JWT token.</returns>
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
_authService = authService;
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
if (keyValuePairs == null)
{
throw new InvalidOperationException("Failed to parse JWT token.");
}
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString() ?? string.Empty));
}
/// <summary>
/// Gets the authentication state asynchronously.
/// </summary>
/// <returns>The authentication state.</returns>
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
string token = await _authService.GetAccessTokenAsync();
string token = await authService.GetAccessTokenAsync();
var identity = new ClaimsIdentity();
@@ -31,7 +55,7 @@ public class CustomAuthStateProvider : AuthenticationStateProvider
catch (Exception)
{
Console.WriteLine("Invalid JWT token. Removing...");
await _authService.RemoveTokensAsync();
await authService.RemoveTokensAsync();
identity = new ClaimsIdentity();
}
}
@@ -44,14 +68,11 @@ public class CustomAuthStateProvider : AuthenticationStateProvider
return state;
}
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
/// <summary>
/// Parses the base64 string without padding.
/// </summary>
/// <param name="base64">The base64 string.</param>
/// <returns>The byte array parsed from the base64 string.</returns>
private static byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
@@ -59,6 +80,7 @@ public class CustomAuthStateProvider : AuthenticationStateProvider
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}

View File

@@ -1,19 +1,28 @@
using Microsoft.AspNetCore.Components;
//-----------------------------------------------------------------------
// <copyright file="AliasVaultApiHandlerService.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.WebApp.Auth.Services;
using System.Net;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components;
public class AliasVaultApiHandlerService : DelegatingHandler
/// <summary>
/// This services handles all API requests to the AliasVault API and will add the access token to the request headers.
/// If a 401 unauthorized is returned by the API it will intercept this response and attempt to automatically refresh the access token.
/// </summary>
public class AliasVaultApiHandlerService(IServiceProvider serviceProvider) : DelegatingHandler
{
private readonly IServiceProvider _serviceProvider;
public AliasVaultApiHandlerService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// Override the SendAsync method to add the access token to the request headers.
/// </summary>
/// <param name="request">HttpRequestMessage instance.</param>
/// <param name="cancellationToken">CancellationToken instance.</param>
/// <returns>HttpResponseMessage.</returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Check if the request already contains the refreshed token to prevent infinite loop
@@ -25,7 +34,7 @@ public class AliasVaultApiHandlerService : DelegatingHandler
}
// Set the access token in the Authorization header
var authService = _serviceProvider.GetRequiredService<AuthService>();
var authService = serviceProvider.GetRequiredService<AuthService>();
var token = await authService.GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
@@ -39,21 +48,20 @@ public class AliasVaultApiHandlerService : DelegatingHandler
{
// Retry the original request with the new access token
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
// Add a custom header to indicate that this is a retry attempt
// Add a custom header to indicate that the next request is a retry attempt and any failure should be ignored.
request.Headers.Add("X-Ignore-Failure", "true");
response = await base.SendAsync(request, cancellationToken);
return response;
}
else
{
// Refreshing token failed. This might be caused by the refresh token itself expired or has been revoked.
// Remove token from localstorage and redirect to login.
// Refreshing token failed. This might be caused by the expiration or revocation of the refresh token itself.
// Remove the token from local storage and redirect to the login page.
await authService.RemoveTokensAsync();
// Redirect to the login page.
var navigationManager = _serviceProvider.GetRequiredService<NavigationManager>();
var navigationManager = serviceProvider.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo("/user/login");
}
}

View File

@@ -1,27 +1,43 @@
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.Authorization;
//-----------------------------------------------------------------------
// <copyright file="AuthService.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.WebApp.Auth.Services;
using AliasVault.Shared.Models;
using Blazored.LocalStorage;
using System.Net.Http.Json;
using System.Text.Json;
using AliasVault.Shared.Models;
using Blazored.LocalStorage;
/// <summary>
/// This service is responsible for handling authentication-related operations such as refreshing tokens,
/// storing tokens, and revoking tokens.
/// </summary>
public class AuthService
{
private readonly HttpClient _httpClient;
private readonly 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.
/// </summary>
/// <returns>The new access token.</returns>
public async Task<string?> RefreshTokenAsync()
{
// Your logic to get the refresh token and request a new access token
@@ -30,8 +46,9 @@ public class AuthService
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/refresh")
{
Content = JsonContent.Create(tokenInput)
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);
@@ -55,44 +72,47 @@ public class AuthService
}
/// <summary>
/// Retrieve the stored refresh token (e.g., from local storage or a secure place).
/// Retrieves the stored access token asynchronously.
/// </summary>
/// <returns></returns>
/// <returns>The stored access token.</returns>
public async Task<string> GetAccessTokenAsync()
{
return await _localStorage.GetItemAsStringAsync(AccessTokenKey);
return await _localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
}
/// <summary>
/// Store the new access token (e.g., in local storage)
/// Stores the new access token asynchronously.
/// </summary>
/// <param name="newToken"></param>
/// <param name="newToken">The new access token.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task StoreAccessTokenAsync(string newToken)
{
await _localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
}
/// <summary>
/// Retrieve the stored refresh token (e.g., from local storage or a secure place).
/// Retrieves the stored refresh token asynchronously.
/// </summary>
/// <returns></returns>
/// <returns>The stored refresh token.</returns>
public async Task<string> GetRefreshTokenAsync()
{
return await _localStorage.GetItemAsStringAsync(RefreshTokenKey);
return await _localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
}
/// <summary>
/// Store the new access token (e.g., in local storage).
/// Stores the new refresh token asynchronously.
/// </summary>
/// <param name="newToken"></param>
/// <param name="newToken">The new refresh token.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task StoreRefreshTokenAsync(string newToken)
{
await _localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
}
/// <summary>
/// Remove the stored access and refresh tokens, called when logging out.
/// Removes the stored access and refresh tokens asynchronously, called when logging out.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task RemoveTokensAsync()
{
await _localStorage.RemoveItemAsync(AccessTokenKey);
@@ -111,15 +131,22 @@ public class AuthService
}
/// <summary>
/// Revoke the access and refresh tokens on the server.
/// Revokes the access and refresh tokens on the server asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task RevokeTokenAsync()
{
var tokenInput = new TokenModel { Token = await GetAccessTokenAsync(), RefreshToken = await GetRefreshTokenAsync() };
var tokenInput = new TokenModel
{
Token = await GetAccessTokenAsync(),
RefreshToken = await GetRefreshTokenAsync(),
};
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/revoke")
{
Content = JsonContent.Create(tokenInput)
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);

View File

@@ -12,18 +12,10 @@
@code {
[Parameter]
public AliasVault.Shared.Models.WebApi.AliasListEntry Obj { get; set; } = new();
private bool showModal = false;
private void ShowDetails()
{
// Redirect to view page instead for now.
NavigationManager.NavigateTo($"/alias/{Obj.Id}");
//showModal = true;
}
private void CloseDetails()
{
showModal = false;
}
}

View File

@@ -16,8 +16,8 @@
</div>
@code {
[Parameter] public string Label { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public string Label { get; set; } = "Value";
[Parameter] public string Value { get; set; } = string.Empty;
private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId;
private string _inputId = Guid.NewGuid().ToString();

View File

@@ -8,15 +8,29 @@
</div>
@code {
[Parameter] public string Label { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
/// <summary>
/// Label for the input field.
/// </summary>
[Parameter]
public string Label { get; set; } = "Value";
/// <summary>
/// Value of the input field.
/// </summary>
[Parameter]
public string Value { get; set; } = string.Empty;
/// <summary>
/// Callback that is triggered when the value changes.
/// </summary>
[Parameter]
public EventCallback<string?> ValueChanged { get; set; }
private string _inputId = Guid.NewGuid().ToString();
private async Task OnInputChanged(ChangeEventArgs e)
{
Value = e.Value.ToString();
Value = e.Value?.ToString() ?? string.Empty;
await ValueChanged.InvokeAsync(Value);
}
}

View File

@@ -1,4 +1,5 @@
@using BlazorServer.Models.Spamok
@using AliasVault.WebApp.Pages.Aliases.Models.Spamok
@using BlazorServer.Models.Spamok
@inherits ComponentBase
@inject IHttpClientFactory HttpClientFactory
@@ -56,23 +57,19 @@ else
@code {
[Parameter]
public string EmailPrefix { get; set; }
public string EmailPrefix { get; set; } = string.Empty;
[Parameter]
public List<MailboxEmailApiModel> MailboxEmails { get; set; } = new List<MailboxEmailApiModel>();
public bool IsLoading { get; set; } = true;
protected override Task OnAfterRenderAsync(bool firstRender)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
base.OnAfterRenderAsync(firstRender);
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
LoadRecentEmailsAsync();
await LoadRecentEmailsAsync();
}
return Task.CompletedTask;
}
private async Task LoadRecentEmailsAsync()
@@ -81,9 +78,9 @@ else
StateHasChanged();
var client = HttpClientFactory.CreateClient("EmailClient");
MailboxApiModel mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{EmailPrefix}");
MailboxApiModel? mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{EmailPrefix}");
if (mailbox.Mails != null)
if (mailbox?.Mails != null)
{
MailboxEmails = mailbox.Mails;
}
@@ -91,5 +88,4 @@ else
IsLoading = false;
StateHasChanged();
}
}

View File

@@ -1,7 +1,24 @@
//-----------------------------------------------------------------------
// <copyright file="BreadcrumbItem.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.WebApp.Components.Models;
/// <summary>
/// Represents a breadcrumb item for the breadcrumb component.
/// </summary>
public class BreadcrumbItem
{
/// <summary>
/// Gets or sets the display name for the breadcrumb item.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Gets or sets the URL for the breadcrumb item.
/// </summary>
public string? Url { get; set; }
}

View File

@@ -1,6 +1,6 @@
@code {
[Inject] private NavigationManager Navigation { get; set; }
@inject NavigationManager Navigation
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo("/user/login");

View File

@@ -173,6 +173,8 @@ else
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (EditMode)
{
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Edit alias" });
@@ -189,8 +191,21 @@ else
{
if (EditMode)
{
if (Id is null)
{
Navigation.NavigateTo("/404");
return;
}
// Load existing obj, retrieve from service
obj = await AliasService.LoadAliasAsync(Id.Value);
var alias = await AliasService.LoadAliasAsync(Id.Value);
if (alias is null)
{
Navigation.NavigateTo("/404");
return;
}
alias = obj;
}
else
{
@@ -255,7 +270,7 @@ else
// Try to extract favicon from service URL
// TODO: Fix favicon extraction
/*if (obj.Service.Url != null && !string.IsNullOrEmpty(obj.Service.Url))
{
{
obj.Service.Logo = await FaviconExtractor.FaviconService.GetFaviconAsync(obj.Service.Url);
} */
@@ -275,7 +290,10 @@ else
if (EditMode)
{
await AliasService.UpdateAliasAsync(obj, Id.Value);
if (Id is not null)
{
await AliasService.UpdateAliasAsync(obj, Id.Value);
}
}
else
{

View File

@@ -19,7 +19,7 @@
@if (IsLoading)
{
<FullPageLoadingAnimation />
<LoadingIndicator />
}
else
{
@@ -53,6 +53,7 @@ else
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { Url = "alias/" + Id, DisplayName = "View Alias" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete alias" });
}

View File

@@ -1,10 +1,39 @@
//-----------------------------------------------------------------------
// <copyright file="AttachmentApiModel.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 BlazorServer.Models.Spamok;
/// <summary>
/// Represents an attachment for an email.
/// </summary>
public class AttachmentApiModel
{
/// <summary>
/// Gets or sets the ID of the attachment.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the ID of the email the attachment belongs to.
/// </summary>
public int Email_Id { get; set; }
public string Filename { get; set; }
public string MimeType { get; set; }
/// <summary>
/// Gets or sets the filename of the attachment.
/// </summary>
public string Filename { get; set; } = null!;
/// <summary>
/// Gets or sets the MIME type of the attachment.
/// </summary>
public string MimeType { get; set; } = null!;
/// <summary>
/// Gets or sets the size of the attachment in bytes.
/// </summary>
public int Filesize { get; set; }
}
}

View File

@@ -1,18 +1,79 @@
//-----------------------------------------------------------------------
// <copyright file="EmailApiModel.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 BlazorServer.Models.Spamok;
/// <summary>
/// Represents an email API model.
/// </summary>
public class EmailApiModel
{
/// <summary>
/// Gets or sets the ID of the email.
/// </summary>
public int Id { get; set; }
public string Subject { get; set; }
public string FromDisplay { get; set; }
public string FromDomain { get; set; }
public string FromLocal { get; set; }
public string ToDomain { get; set; }
public string ToLocal { get; set; }
/// <summary>
/// Gets or sets the subject of the email.
/// </summary>
public string Subject { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the display name of the sender.
/// </summary>
public string FromDisplay { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the domain of the sender's email address.
/// </summary>
public string FromDomain { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the local part of the sender's email address.
/// </summary>
public string FromLocal { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the domain of the recipient's email address.
/// </summary>
public string ToDomain { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the local part of the recipient's email address.
/// </summary>
public string ToLocal { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the date of the email.
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// Gets or sets the system date of the email.
/// </summary>
public DateTime DateSystem { get; set; }
/// <summary>
/// Gets or sets the number of seconds ago the email was received.
/// </summary>
public double SecondsAgo { get; set; }
/// <summary>
/// Gets or sets the HTML content of the email message.
/// </summary>
public string? MessageHtml { get; set; }
/// <summary>
/// Gets or sets the plain text content of the email message.
/// </summary>
public string? MessagePlain { get; set; }
public List<AttachmentApiModel> Attachments { get; set; }
}
/// <summary>
/// Gets or sets the list of attachments in the email.
/// </summary>
public List<AttachmentApiModel> Attachments { get; set; } = new();
}

View File

@@ -1,8 +1,29 @@
namespace BlazorServer.Models.Spamok;
//-----------------------------------------------------------------------
// <copyright file="MailboxApiModel.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.WebApp.Pages.Aliases.Models.Spamok;
/// <summary>
/// Represents a mailbox API model.
/// </summary>
public class MailboxApiModel
{
public string Address { get; set; }
/// <summary>
/// Gets or sets the address of the mailbox.
/// </summary>
public string Address { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the mailbox is subscribed.
/// </summary>
public bool Subscribed { get; set; }
public List<MailboxEmailApiModel> Mails { get; set; }
}
/// <summary>
/// Gets or sets the list of mailbox email API models.
/// </summary>
public List<MailboxEmailApiModel> Mails { get; set; } = new();
}

View File

@@ -1,16 +1,69 @@
namespace BlazorServer.Models.Spamok;
//-----------------------------------------------------------------------
// <copyright file="MailboxEmailApiModel.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.WebApp.Pages.Aliases.Models.Spamok;
/// <summary>
/// Represents a mailbox email API model.
/// </summary>
public class MailboxEmailApiModel
{
/// <summary>
/// Gets or sets the ID of the email.
/// </summary>
public int Id { get; set; }
public string Subject { get; set; }
public string FromDisplay { get; set; }
public string FromDomain { get; set; }
public string FromLocal { get; set; }
public string ToDomain { get; set; }
public string ToLocal { get; set; }
/// <summary>
/// Gets or sets the subject of the email.
/// </summary>
public string Subject { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the display name of the sender.
/// </summary>
public string FromDisplay { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the domain of the sender's email address.
/// </summary>
public string FromDomain { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the local part of the sender's email address.
/// </summary>
public string FromLocal { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the domain of the recipient's email address.
/// </summary>
public string ToDomain { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the local part of the recipient's email address.
/// </summary>
public string ToLocal { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the date of the email.
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// Gets or sets the system date of the email.
/// </summary>
public DateTime DateSystem { get; set; }
public string MessagePreview { get; set; }
/// <summary>
/// Gets or sets the preview of the email message.
/// </summary>
public string MessagePreview { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the number of seconds ago the email was received.
/// </summary>
public double SecondsAgo { get; set; }
}
}

View File

@@ -64,7 +64,7 @@ else
<form action="#">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<CopyPasteFormRow Label="Initials" Value="@(Alias.Identity.FirstName.Substring(0,1))"></CopyPasteFormRow>
<CopyPasteFormRow Label="Initials" Value="@(Alias.Identity.FirstName?.Substring(0,1))"></CopyPasteFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<CopyPasteFormRow Label="First name" Value="@(Alias.Identity.FirstName)"></CopyPasteFormRow>

View File

@@ -1,10 +1,17 @@
using AliasVault.WebApp.Components.Models;
//-----------------------------------------------------------------------
// <copyright file="PageBase.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.WebApp.Pages.Base;
using AliasVault.WebApp.Components.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.JSInterop;
namespace AliasVault.WebApp.Pages.Base;
/// <summary>
/// Base authorize page that all pages that are part of the logged in website should inherit from.
/// All pages that inherit from this class will require the user to be logged in and have a confirmed email.
@@ -12,22 +19,35 @@ namespace AliasVault.WebApp.Pages.Base;
/// </summary>
public class PageBase : OwningComponentBase
{
private bool _parametersInitialSet;
/// <summary>
/// Gets or sets the NavigationManager.
/// </summary>
[Inject]
public NavigationManager NavigationManager { get; set; } = null!;
/// <summary>
/// Gets or sets the AuthenticationStateProvider.
/// </summary>
[Inject]
public AuthenticationStateProvider AuthStateProvider { get; set; } = null!;
/// <summary>
/// Gets or sets the IJSRuntime.
/// </summary>
[Inject]
public IJSRuntime Js { get; set; } = null!;
/// <summary>
/// Contains the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method.
/// 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>();
private bool _parametersInitialSet;
/// <summary>
/// Initializes the component asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
@@ -37,28 +57,32 @@ public class PageBase : OwningComponentBase
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationManager.BaseUri });
// Detect success messages in query string and add them to the SuccessMessages list
// TODO: Implement this with example for default add/edit update action...
var uri = new Uri(NavigationManager.Uri);
}
/// <summary>
/// Get username from the authentication state.
/// Gets the username from the authentication state asynchronously.
/// </summary>
/// <returns></returns>
/// <returns>The username.</returns>
protected async Task<string> GetUsernameAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
return authState.User?.Identity?.Name ?? "[Unknown]";
}
/// <summary>
/// Sets the parameters asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
// This is needed to prevent the OnParametersSetAsync method from running together with OnInitialized on initial page load.
// This is to prevent the OnParametersSetAsync method from running together with OnInitialized on initial page load.
if (!_parametersInitialSet)
{
_parametersInitialSet = true;
return;
}
}
}

View File

@@ -51,8 +51,16 @@
StateHasChanged();
// Load the aliases from the webapi via AliasService.
Aliases = await AliasService.GetListAsync();
var aliasListEntries = await AliasService.GetListAsync();
if (aliasListEntries is null)
{
// Error loading aliases.
IsLoading = false;
StateHasChanged();
return;
}
Aliases = aliasListEntries;
IsLoading = false;
StateHasChanged();
}

View File

@@ -1,36 +1,42 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
//-----------------------------------------------------------------------
// <copyright file="Program.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>
//-----------------------------------------------------------------------
using AliasVault.WebApp;
using AliasVault.WebApp.Auth.Providers;
using AliasVault.WebApp.Auth.Services;
using AliasVault.WebApp.Services;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using AliasVault.WebApp;
using AliasVault.WebApp.Services;
using AliasVault.WebApp.Auth.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddTransient<AliasVaultApiHandlerService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddHttpClient("AliasVault.Api")
.AddHttpMessageHandler<AliasVaultApiHandlerService>();
builder.Services.AddHttpClient("AliasVault.Api").AddHttpMessageHandler<AliasVaultApiHandlerService>();
builder.Services.AddScoped(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("AliasVault.Api");
httpClient.BaseAddress = new Uri(builder.Configuration["ApiUrl"]);
if (builder.Configuration["ApiUrl"] is null)
{
throw new InvalidOperationException("The 'ApiUrl' configuration value is required.");
}
httpClient.BaseAddress = new Uri(builder.Configuration["ApiUrl"]!);
return httpClient;
});
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddTransient<AliasVaultApiHandlerService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<AliasService>();
builder.Services.AddSingleton<ClipboardCopyService>();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();

View File

@@ -1,48 +1,50 @@
using System.Net.Http.Json;
using AliasDb;
using AliasVault.Shared.Models.WebApi;
using Identity = AliasGenerators.Identity.Models.Identity;
//-----------------------------------------------------------------------
// <copyright file="AliasService.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.WebApp.Services;
public class AliasService
using System.Net.Http.Json;
using AliasVault.Shared.Models.WebApi;
using Identity = AliasGenerators.Identity.Models.Identity;
/// <summary>
/// Service class for alias operations.
/// </summary>
public class AliasService(HttpClient httpClient)
{
private HttpClient _httpClient;
/// <summary>
/// Public constructor which can be called from static async method or directly.
/// </summary>
/// <param name="aliasObj"></param>
/// <param name="httpClient"></param>
public AliasService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Generate random identity by calling the IdentityGenerator API.
/// </summary>
/// <returns></returns>
/// <returns>Identity object.</returns>
public async Task<Identity> GenerateRandomIdentityAsync()
{
return await _httpClient.GetFromJsonAsync<Identity>("api/Identity/generate");
var identity = await httpClient.GetFromJsonAsync<Identity>("api/Identity/generate");
if (identity == null)
{
throw new InvalidOperationException("Failed to generate random identity.");
}
return identity;
}
/// <summary>
/// Insert new entry into database.
/// </summary>
/// <param name="aliasObject"></param>
/// <param name="aliasObject">Alias object to insert.</param>
/// <returns>Guid of inserted entry.</returns>
public async Task<Guid> InsertAliasAsync(Alias aliasObject)
{
// Put to webapi.
try
{
var returnObject = await _httpClient.PutAsJsonAsync<Alias>("api/Alias", aliasObject);
var returnObject = await httpClient.PutAsJsonAsync<Alias>("api/Alias", aliasObject);
return await returnObject.Content.ReadFromJsonAsync<Guid>();
}
catch
{
// Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page.
return Guid.Empty;
}
}
@@ -50,19 +52,18 @@ public class AliasService
/// <summary>
/// Update an existing entry to database.
/// </summary>
/// <param name="aliasObject"></param>
/// <param name="id"></param>
/// <param name="aliasObject">Alias object to update.</param>
/// <param name="id">Id of alias to update.</param>
/// <returns>Guid of updated entry.</returns>
public async Task<Guid> UpdateAliasAsync(Alias aliasObject, Guid id)
{
// Post to webapi.
try
{
var returnObject = await _httpClient.PostAsJsonAsync<Alias>("api/Alias/" + id, aliasObject);
var returnObject = await httpClient.PostAsJsonAsync<Alias>("api/Alias/" + id, aliasObject);
return await returnObject.Content.ReadFromJsonAsync<Guid>();
}
catch
{
// Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page.
return Guid.Empty;
}
}
@@ -70,48 +71,47 @@ public class AliasService
/// <summary>
/// Load existing entry from database.
/// </summary>
/// <param name="aliasId"></param>
/// <param name="aliasId">Id of alias to load.</param>
/// <returns>Alias object.</returns>
public async Task<Alias?> LoadAliasAsync(Guid aliasId)
{
// Make webapi call to get list of all entries.
try
{
return await _httpClient.GetFromJsonAsync<Alias>("api/Alias/" + aliasId);
return await httpClient.GetFromJsonAsync<Alias>("api/Alias/" + aliasId);
}
catch
{
// Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page.
return null;
}
}
/// <summary>
/// Get list with all entries from database.
/// Get list with all entries that belong to current user.
/// </summary>
/// <returns>List of AliasListEntry objects.</returns>
public async Task<List<AliasListEntry>?> GetListAsync()
{
// Make webapi call to get list of all entries.
try
{
return await _httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/Alias/items");
return await httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/Alias/items");
}
catch
{
// Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page.
return null;
return new List<AliasListEntry>();
}
}
/// <summary>
/// Removes existing entry from database.
/// </summary>
/// <param name="alias"></param>
public async Task DeleteAliasAsync(Guid Id)
/// <param name="id">Id of alias to delete.</param>
/// <returns>Task.</returns>
public async Task DeleteAliasAsync(Guid id)
{
// Delete from webapi.
try
{
await _httpClient.DeleteAsync("api/Alias/" + Id);
await httpClient.DeleteAsync("api/Alias/" + id);
}
catch
{

View File

@@ -1,3 +1,10 @@
//-----------------------------------------------------------------------
// <copyright file="ClipboardCopyService.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.WebApp.Services;
/// <summary>
@@ -5,18 +12,26 @@ namespace AliasVault.WebApp.Services;
/// </summary>
public class ClipboardCopyService
{
private string _currentCopiedId;
public event Action<string> OnCopy;
private string _currentCopiedId = string.Empty;
/// <summary>
/// Event to notify the application that an item has been copied.
/// </summary>
public event Action<string> OnCopy = null!;
/// <summary>
/// Keep track of the last copied item.
/// </summary>
/// <param name="id"></param>
/// <param name="id">Id of the last copied item.</param>
public void SetCopied(string id)
{
_currentCopiedId = id;
OnCopy?.Invoke(_currentCopiedId);
}
/// <summary>
/// Get the last copied item.
/// </summary>
/// <returns>Id of last copied item.</returns>
public string GetCopiedId() => _currentCopiedId;
}

View File

@@ -6,6 +6,16 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.xml</DocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.xml</DocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AliasGenerators\AliasGenerators.csproj" />
<ProjectReference Include="..\AliasDb\AliasDb.csproj" />
@@ -16,18 +26,15 @@
<Content Include="..\..\LICENSE.md">
<Link>LICENSE.md</Link>
</Content>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Include="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -269,7 +269,7 @@ else
// Try to extract favicon from service URL
if (obj.Service.Url != null && !obj.Service.Url.IsNullOrEmpty())
{
obj.Service.Logo = await FaviconExtractor.FaviconService.GetFaviconAsync(obj.Service.Url);
obj.Service.Logo = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(obj.Service.Url);
}
if (EditMode)

View File

@@ -1,19 +0,0 @@
{
"settings": {
"documentationRules": {
"companyName": "lanedirt",
"xmlHeader": true,
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
"variables": {
"licenseName": "MIT",
"licenseFile": "LICENSE.md"
},
"headerDecoration": "-----------------------------------------------------------------------"
},
"indentation": {
"indentationSize": 4,
"tabSize": 4,
"useTabs": false
}
}
}

View File

@@ -1,71 +1,26 @@
using Microsoft.Playwright;
//-----------------------------------------------------------------------
// <copyright file="AliasTests.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.E2ETests;
/// <summary>
/// End-to-end tests for the alias management.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class AliasTests : PlaywrightTest
{
/// <summary>
/// Test if registering a new account works.
/// </summary>
[Test]
public async Task AliasListingCorrect()
{
var navigationTask = Page.WaitForNavigationAsync();
await Page.GotoAsync(_appBaseUrl + "aliases");
await navigationTask;
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=AliasVault");
// Check if the expected content is present.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Find all of your aliases below"), "No index content after logging in.");
}
private static readonly Random Random = new();
/// <summary>
/// Test if registering a new account works.
/// Helper method to fill all input fields on a page with random data.
/// </summary>
[Test]
public async Task CreateAlias()
{
var navigationTask = Page.WaitForNavigationAsync();
await Page.GotoAsync(_appBaseUrl + "add-alias");
await navigationTask;
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=AliasVault");
// Check if a button with text "Generate Random Identity" appears
var generateButton = Page.Locator("text=Generate Random Identity");
Assert.That(generateButton, Is.Not.Null, "Generate button not found.");
// Fill all input fields with random data
await FillAllInputFields(Page);
// Press submit button with text "Create Alias"
var submitButton = Page.Locator("text=Save Alias").First;
navigationTask = Page.WaitForNavigationAsync(new PageWaitForNavigationOptions() { Timeout = 200000 });
await submitButton.ClickAsync();
await navigationTask;
// Check if the redirection occurred
var currentUrl = Page.Url;
Assert.That(currentUrl, Does.Contain(_appBaseUrl + "alias/"));
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=Login credentials");
// Check if the alias was created
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Login credentials"), "Alias not created.");
// TODO: Implement proper data input and verification if what was created is correct.
}
private static readonly Random Random = new Random();
/// <param name="page">IPage instance where to fill the input fields for.</param>
/// <returns>Async task.</returns>
public static async Task FillAllInputFields(IPage page)
{
// Locate all input fields
@@ -86,13 +41,66 @@ public class AliasTests : PlaywrightTest
"email" => GenerateRandomEmail(),
"number" => GenerateRandomNumber(),
"password" => GenerateRandomPassword(),
_ => GenerateRandomString() // Default for text, search, etc.
_ => GenerateRandomString(), // Default for all other types
};
await input.FillAsync(randomData);
}
}
/// <summary>
/// Test if the alias listing index page works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task AliasListingCorrect()
{
await Page.GotoAsync(AppBaseUrl + "aliases");
await WaitForURLAsync("**/aliases");
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=AliasVault");
// Check if the expected content is present.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Find all of your aliases below"), "No index content after logging in.");
}
/// <summary>
/// Test if creating a new alias works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task CreateAlias()
{
await Page.GotoAsync(AppBaseUrl + "add-alias");
await WaitForURLAsync("**/add-alias");
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=AliasVault");
// Check if a button with text "Generate Random Identity" appears
var generateButton = Page.Locator("text=Generate Random Identity");
Assert.That(generateButton, Is.Not.Null, "Generate button not found.");
// Fill all input fields with random data
await FillAllInputFields(Page);
// Press submit button with text "Create Alias"
var submitButton = Page.Locator("text=Save Alias").First;
await submitButton.ClickAsync();
await WaitForURLAsync("**/alias/**");
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=Login credentials");
// Check if the alias was created
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Login credentials"), "Alias not created.");
// TODO: Implement proper data input and verification if what was created is correct.
}
private static string GenerateRandomString(int length = 10)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

View File

@@ -9,14 +9,38 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\AliasVault.E2ETests.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\AliasVault.E2ETests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.27.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.44.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,17 @@
//-----------------------------------------------------------------------
// <copyright file="AuthTests.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>
//-----------------------------------------------------------------------
using Microsoft.Playwright;
namespace AliasVault.E2ETests;
/// <summary>
/// End-to-end tests for authentication.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class AuthTests : PlaywrightTest
@@ -9,20 +19,19 @@ public class AuthTests : PlaywrightTest
/// <summary>
/// Test if registering a new account works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task LogoutAndLogin()
{
// Logout
var navigationTask = Page.WaitForNavigationAsync();
await Page.GotoAsync(_appBaseUrl + "user/logout");
await navigationTask;
// Logout.
await Page.GotoAsync(AppBaseUrl + "user/logout");
await Page.WaitForURLAsync("**/user/logout", new PageWaitForURLOptions() { Timeout = 2000 });
// Wait for the content to load.
await Page.WaitForSelectorAsync("text=AliasVault");
// Check that we got redirected to /user/login
var currentUrl = Page.Url;
Assert.That(currentUrl, Is.EqualTo(_appBaseUrl + "user/login"));
// Wait and check if we get redirected to /user/login.
await Page.WaitForURLAsync("**/user/login", new PageWaitForURLOptions() { Timeout = 2000 });
await Login();
}
@@ -30,31 +39,25 @@ public class AuthTests : PlaywrightTest
/// <summary>
/// Test if logging in works.
/// </summary>
/// <returns>Async task.</returns>
public async Task Login()
{
await Page.GotoAsync(_appBaseUrl);
var navigationTask = Page.WaitForNavigationAsync();
await navigationTask;
await Page.GotoAsync(AppBaseUrl);
// Check that we got redirected to /user/login
var currentUrl = Page.Url;
Assert.That(currentUrl, Is.EqualTo(_appBaseUrl + "user/login"));
// Check that we are on the login page after navigating to the base URL.
// We are expecting to not be authenticated and thus to be redirected to the login page.
await WaitForURLAsync("**/user/login");
// Try to login with test credentials.
var emailField = Page.Locator("input[id='email']");
var passwordField = Page.Locator("input[id='password']");
await emailField.FillAsync(_randomEmail);
await passwordField.FillAsync(_randomPassword);
await emailField.FillAsync(TestUserEmail);
await passwordField.FillAsync(TestUserPassword);
// Check if we get redirected when clicking on the login button.
var loginButton = Page.Locator("button[type='submit']");
navigationTask = Page.WaitForNavigationAsync(new PageWaitForNavigationOptions() { Timeout = 200000});
await loginButton.ClickAsync();
await navigationTask;
// Check if the redirection occurred
currentUrl = Page.Url;
Assert.That(currentUrl, Is.EqualTo(_appBaseUrl));
await WaitForURLAsync(AppBaseUrl);
// Check if the login was successful by verifying content.
var pageContent = await Page.TextContentAsync("body");

View File

@@ -0,0 +1,179 @@
//-----------------------------------------------------------------------
// <copyright file="PlaywrightTest.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.E2ETests.Common;
using Microsoft.Playwright;
/// <summary>
/// Base class for tests that use Playwright for E2E browser testing.
/// </summary>
public class PlaywrightTest
{
/// <summary>
/// For starting the WebAPI project in-memory.
/// </summary>
private readonly WebApplicationFactoryFixture<AliasVault.Api.Program> _factory = new();
/// <summary>
/// The BlazorWasmAppManager instance.
/// </summary>
private BlazorWasmAppManager _blazorWasmAppManager;
/// <summary>
/// Gets or sets base URL where the Blazor WASM app runs on including random port.
/// </summary>
protected string AppBaseUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets random unique account email that is used for the test.
/// </summary>
protected string TestUserEmail { get; set; } = string.Empty;
/// <summary>
/// Gets or sets random unique account password that is used for the test.
/// </summary>
protected string TestUserPassword { get; set; } = string.Empty;
/// <summary>
/// Gets the Playwright browser instance.
/// </summary>
protected IBrowser Browser { get; private set; }
/// <summary>
/// Gets the Playwright browser context.
/// </summary>
protected IBrowserContext Context { get; private set; }
/// <summary>
/// Gets the Playwright page.
/// </summary>
protected IPage Page { get; private set; }
/// <summary>
/// One time setup for the Playwright test which runs before all tests in the class.
/// </summary>
/// <returns>Async task.</returns>
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
// Determine random port for the WebAPI between 5100-5900. The WASM app will run on the next port.
var apiPort = new Random().Next(5100, 5900);
var appPort = apiPort + 1;
AppBaseUrl = "http://localhost:" + appPort + "/";
// Start WebAPI in-memory.
_factory.HostUrl = "http://localhost:" + apiPort;
_factory.CreateDefaultClient();
// Start Blazor WASM app out-of-process.
_blazorWasmAppManager = new BlazorWasmAppManager();
await _blazorWasmAppManager.StartBlazorWasmAsync(appPort);
// Set Playwright headless mode true if not in debug mode.
bool isDebugMode = System.Diagnostics.Debugger.IsAttached;
bool headless = !isDebugMode;
var playwright = await Playwright.CreateAsync();
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = headless });
Context = await Browser.NewContextAsync();
// Intercept Blazor WASM app requests to override appsettings.json
await Context.RouteAsync("**/appsettings.json", async route =>
{
var response = new
{
ApiUrl = "http://localhost:" + apiPort,
};
await route.FulfillAsync(new RouteFulfillOptions
{
ContentType = "application/json",
Body = System.Text.Json.JsonSerializer.Serialize(response),
});
});
Page = await Context.NewPageAsync();
// Register a new account via the UI
await Register();
}
/// <summary>
/// Tear down the Playwright test which runs after all tests are done in the class.
/// </summary>
/// <returns>Async task.</returns>
[OneTimeTearDown]
public async Task OneTimeTearDown()
{
await Page.CloseAsync();
await Context.CloseAsync();
await Browser.CloseAsync();
await _factory.DisposeAsync();
_blazorWasmAppManager.StopBlazorWasm();
}
/// <summary>
/// Wait for the specified URL to be loaded with a default timeout.
/// </summary>
/// <param name="url">The URL to wait for. This may also contains wildcard such as "**/user/login".</param>
/// <returns>Async task.</returns>
protected async Task WaitForURLAsync(string url)
{
await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = TestDefaults.DefaultTimeout });
}
/// <summary>
/// Wait for the specified URL to be loaded with a custom timeout.
/// </summary>
/// <param name="url">The URL to wait for. This may also contains wildcard such as "**/user/login".</param>
/// <param name="timeoutInMs">Custom timeout in milliseconds.</param>
/// <returns>Async task.</returns>
protected async Task WaitForURLAsync(string url, int timeoutInMs)
{
await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = timeoutInMs });
}
/// <summary>
/// Register a new random account.
/// </summary>
/// <returns>Async task.</returns>
private async Task Register()
{
// Generate random email and password
TestUserEmail = $"{Guid.NewGuid().ToString()}@test.com";
TestUserPassword = Guid.NewGuid().ToString();
// Check that we get redirected to /user/login when accessing the root URL and not authenticated.
await Page.GotoAsync(AppBaseUrl);
await WaitForURLAsync("**/user/login", 20000);
// Try to register a new account.
var registerButton = Page.Locator("a[href='/user/register']");
await registerButton.ClickAsync();
await WaitForURLAsync("**/user/register");
// Try to login with test credentials.
var emailField = Page.Locator("input[id='email']");
var passwordField = Page.Locator("input[id='password']");
var password2Field = Page.Locator("input[id='password2']");
await emailField.FillAsync(TestUserEmail);
await passwordField.FillAsync(TestUserPassword);
await password2Field.FillAsync(TestUserPassword);
// Check the terms of service checkbox
var termsCheckbox = Page.Locator("input[id='terms']");
await termsCheckbox.CheckAsync();
// Check if we get redirected when clicking on the register button.
var submitButton = Page.Locator("button[type='submit']");
await submitButton.ClickAsync();
// Check if we get redirected to the root URL after registration which means we are logged in.
await WaitForURLAsync(AppBaseUrl);
}
}

View File

@@ -0,0 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="TestDefaults.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.E2ETests.Common;
/// <summary>
/// Default values for tests.
/// </summary>
public static class TestDefaults
{
/// <summary>
/// Gets or sets default timeout while waiting for pages to load in milliseconds.
/// </summary>
public static int DefaultTimeout { get; set; } = 5000;
}

View File

@@ -1,2 +1,13 @@
//-----------------------------------------------------------------------
// <copyright file="GlobalUsings.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>
//-----------------------------------------------------------------------
// <auto-generated />
global using System.Threading.Tasks;
global using NUnit.Framework;
global using AliasVault.E2ETests.Infrastructure;
global using AliasVault.E2ETests.Common;
global using Microsoft.Playwright;

View File

@@ -1,21 +1,27 @@
namespace AliasVault.E2ETests;
//-----------------------------------------------------------------------
// <copyright file="BlazorWasmAppManager.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.E2ETests.Infrastructure;
using System.Diagnostics;
using System.Net;
public class WebAppManager
/// <summary>
/// A class for managing the Blazor WebAssembly application in out-of-process mode for E2E testing.
/// </summary>
public class BlazorWasmAppManager
{
private Process _blazorWasmProcess;
private List<string> _blazorWasmErrors = new();
private string GetBaseDirectory()
{
string currentDir = Directory.GetCurrentDirectory();
// Adjust this if your solution directory is different
string baseDir = Directory.GetParent(currentDir).Parent.Parent.FullName;
return baseDir;
}
private readonly List<string> _blazorWasmErrors = [];
private Process? _blazorWasmProcess;
/// <summary>
/// Starts the Blazor WebAssembly application in out-of-process mode.
/// </summary>
/// <param name="port">The port number to run the app under.</param>
/// <returns>Async task.</returns>
public async Task StartBlazorWasmAsync(int port)
{
var projectPath = $"{GetBaseDirectory()}/../../AliasVault.WebApp/AliasVault.WebApp.csproj";
@@ -30,7 +36,7 @@ public class WebAppManager
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
}
},
};
_blazorWasmProcess.OutputDataReceived += (sender, args) =>
@@ -39,11 +45,13 @@ public class WebAppManager
};
_blazorWasmProcess.ErrorDataReceived += (sender, args) =>
{
TestContext.Out.WriteLine(args.Data);
if (args.Data != null && (args.Data.Contains("error") || args.Data.Contains("fail")))
if (args.Data is null)
{
_blazorWasmErrors.Add(args.Data);
return;
}
TestContext.Out.WriteLine(args.Data);
_blazorWasmErrors.Add(args.Data);
};
_blazorWasmProcess.Start();
@@ -53,6 +61,35 @@ public class WebAppManager
await WaitForStartupAsync(port);
}
/// <summary>
/// Stops the Blazor WebAssembly application process.
/// </summary>
public void StopBlazorWasm()
{
if (_blazorWasmProcess is not null && !_blazorWasmProcess.HasExited)
{
#if WINDOWS
KillProcessAndChildrenWindows(_blazorWasmProcess.Id);
#else
KillProcessAndChildrenUnix(_blazorWasmProcess.Id);
#endif
_blazorWasmProcess.Dispose();
}
}
private string GetBaseDirectory()
{
string currentDir = Directory.GetCurrentDirectory();
string baseDir = string.Empty;
var parentDir = Directory.GetParent(currentDir);
if (parentDir?.Parent?.Parent != null)
{
baseDir = parentDir.Parent.Parent.FullName;
}
return baseDir;
}
private async Task WaitForStartupAsync(int port)
{
// Wait for the application to start up
@@ -81,19 +118,6 @@ public class WebAppManager
}
}
public void StopBlazorWasm()
{
if (!_blazorWasmProcess.HasExited)
{
#if WINDOWS
KillProcessAndChildrenWindows(_blazorWasmProcess.Id);
#else
KillProcessAndChildrenUnix(_blazorWasmProcess.Id);
#endif
_blazorWasmProcess.Dispose();
}
}
#if WINDOWS
private void KillProcessAndChildrenWindows(int pid)
{
@@ -128,20 +152,20 @@ public class WebAppManager
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
CreateNoWindow = true,
};
var pkillProcess = new Process
{
StartInfo = startInfo
StartInfo = startInfo,
};
pkillProcess.Start();
pkillProcess.WaitForExit();
}
catch (Exception ex)
catch (Exception e)
{
// Handle exception
Console.WriteLine(e.Message);
}
try
@@ -149,9 +173,9 @@ public class WebAppManager
Process process = Process.GetProcessById(pid);
process.Kill();
}
catch (ArgumentException)
catch (ArgumentException e)
{
// Process already exited.
Console.WriteLine(e.Message);
}
}
#endif

View File

@@ -1,3 +1,12 @@
//-----------------------------------------------------------------------
// <copyright file="WebApplicationFactoryFixture.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.E2ETests.Infrastructure;
using System.Data.Common;
using AliasDb;
using Microsoft.AspNetCore.Hosting;
@@ -7,32 +16,52 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AliasVault.E2ETests;
/// <summary>
/// Web application factory fixture for integration tests.
/// </summary>
/// <typeparam name="TEntryPoint">The entry point.</typeparam>
public class WebApplicationFactoryFixture<TEntryPoint> : WebApplicationFactory<TEntryPoint>
where TEntryPoint : class
{
public string HostUrl { get; set; } = "https://localhost:5001"; // we can use any free port
/// <summary>
/// Gets or sets the URL the web application host will listen on.
/// </summary>
public string HostUrl { get; set; } = "https://localhost:5001";
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseUrls(HostUrl);
builder.ConfigureServices((context, services) =>
{
// Remove the existing AliasDbContext registration.
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<AliasDbContext>));
if (dbContextDescriptor is null)
{
throw new InvalidOperationException(
"No DbContextOptions<AliasDbContext> registered.");
}
services.Remove(dbContextDescriptor);
// Remove the existing DbConnection registration.
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
if (dbConnectionDescriptor is null)
{
throw new InvalidOperationException(
"No DbContextOptions<AliasDbContext> registered.");
}
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
// Create a new DbConnection and AliasDbContext with an in-memory database.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
@@ -49,6 +78,7 @@ public class WebApplicationFactoryFixture<TEntryPoint> : WebApplicationFactory<T
});
}
/// <inheritdoc />
protected override IHost CreateHost(IHostBuilder builder)
{
var dummyHost = builder.Build();

View File

@@ -1,126 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
namespace AliasVault.E2ETests;
using Microsoft.Playwright;
public class PlaywrightTest
{
protected IBrowser Browser { get; private set; }
protected IBrowserContext Context { get; private set; }
protected IPage Page { get; private set; }
private WebAppManager _webAppManager;
protected string _randomEmail = "";
protected string _randomPassword = "";
protected int apiPort = 5001;
protected int appPort = 5000;
protected string _appBaseUrl = "http://localhost:5000/";
/// <summary>
/// For starting the WebAPI project in-memory.
/// </summary>
private WebApplicationFactoryFixture<AliasVaultApiProgram> _factory = new();
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
_webAppManager = new WebAppManager();
// Determine random port for the WebAPI between 5100-5900
apiPort = new Random().Next(5100, 5900);
// Determine random port for the BlazorWasm which is apiPort + 1
appPort = apiPort + 1;
// Update base URL
_appBaseUrl = "http://localhost:" + appPort + "/";
_factory.HostUrl = "http://localhost:" + apiPort;
_factory.CreateDefaultClient();
//await _webAppManager.StartWebApiAsync(5001);
await _webAppManager.StartBlazorWasmAsync(appPort);
var playwright = await Playwright.CreateAsync();
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
Context = await Browser.NewContextAsync();
// Intercept requests and override appsettings.json
await Context.RouteAsync("**/appsettings.json", async route =>
{
var response = new
{
ApiUrl = "http://localhost:" + apiPort
};
await route.FulfillAsync(new RouteFulfillOptions
{
ContentType = "application/json",
Body = System.Text.Json.JsonSerializer.Serialize(response)
});
});
Page = await Context.NewPageAsync();
// Register a new account via the UI
await Register();
}
[OneTimeTearDown]
public async Task OneTimeTearDown()
{
await Page.CloseAsync();
await Context.CloseAsync();
await Browser.CloseAsync();
_factory.Dispose();
//_webAppManager.StopWebApi();
_webAppManager.StopBlazorWasm();
}
/// <summary>
/// Register a new random account.
/// </summary>
public async Task Register()
{
// Generate random email and password
_randomEmail = $"{Guid.NewGuid().ToString()}@test.com";
_randomPassword = Guid.NewGuid().ToString();
await Page.GotoAsync(_appBaseUrl);
var navigationTask = Page.WaitForNavigationAsync();
await navigationTask;
// Check that we got redirected to /user/login
var currentUrl = Page.Url;
Assert.That(currentUrl, Is.EqualTo(_appBaseUrl + "user/login"));
// Try to register a new account.
var registerButton = Page.Locator("a[href='/user/register']");
navigationTask = Page.WaitForNavigationAsync(new PageWaitForNavigationOptions() { Timeout = 2000 });
await registerButton.ClickAsync();
await navigationTask;
// Try to login with test credentials.
var emailField = Page.Locator("input[id='email']");
var passwordField = Page.Locator("input[id='password']");
var password2Field = Page.Locator("input[id='password2']");
await emailField.FillAsync(_randomEmail);
await passwordField.FillAsync(_randomPassword);
await password2Field.FillAsync(_randomPassword);
// Check the terms of service checkbox
var termsCheckbox = Page.Locator("input[id='terms']");
await termsCheckbox.CheckAsync();
// Check if we get redirected when clicking on the register button.
var submitButton = Page.Locator("button[type='submit']");
navigationTask = Page.WaitForNavigationAsync(new PageWaitForNavigationOptions() { Timeout = 200000});
await submitButton.ClickAsync();
await navigationTask;
// Check if the redirection occurred
currentUrl = Page.Url;
Assert.That(currentUrl, Is.EqualTo(_appBaseUrl));
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@@ -10,17 +10,43 @@
<RootNamespace>AliasVault.Tests</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\AliasVault.UnitTests.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\AliasVault.UnitTests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<AdditionalFiles Include="..\..\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Utilities\Cryptography\Cryptography.csproj" />
<ProjectReference Include="..\..\Utilities\FaviconExtractor\FaviconExtractor.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageReference Include="NUnit" Version="4.1.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Utilities\Cryptography\Cryptography.csproj"/>
<ProjectReference Include="..\..\Utilities\FaviconExtractor\FaviconExtractor.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,9 +1,22 @@
using System.Security.Cryptography;
//-----------------------------------------------------------------------
// <copyright file="CryptographyTests.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.Tests;
using System.Security.Cryptography;
/// <summary>
/// Tests for the Cryptography class.
/// </summary>
public class CryptographyTests
{
/// <summary>
/// Common setup for all tests.
/// </summary>
[SetUp]
public void Setup()
{
@@ -32,7 +45,6 @@ public class CryptographyTests
Assert.That(encrypted, Is.Not.Empty);
Assert.That(encrypted, Is.Not.EqualTo(plaintext));
// Decrypt the ciphertext
string decrypted = Cryptography.Cryptography.Decrypt(encrypted, key);
Console.WriteLine($"Decrypted: {decrypted}");
@@ -52,6 +64,7 @@ public class CryptographyTests
// Derive a key from the password using Argon2id
byte[] key = Cryptography.Cryptography.DeriveKeyFromPassword(password, salt);
// Encrypt the plaintext
string encrypted = Cryptography.Cryptography.Encrypt(plaintext, key);

View File

@@ -1,16 +1,32 @@
//-----------------------------------------------------------------------
// <copyright file="FaviconExtractorTests.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.Tests;
/// <summary>
/// Tests for the FaviconExtractor class.
/// </summary>
public class FaviconExtractorTests
{
/// <summary>
/// Common setup for all tests.
/// </summary>
[SetUp]
public void Setup()
{
}
/// <summary>
/// Test extracting a favicon from a known website.
/// </summary>
[Test]
public void ExtractFaviconSpamOK()
{
var faviconBytes = FaviconExtractor.FaviconService.GetFaviconAsync("https://spamok.com");
var faviconBytes = FaviconExtractor.FaviconExtractor.GetFaviconAsync("https://spamok.com");
Assert.That(faviconBytes, Is.Not.Null);
}
}

View File

@@ -1 +1,10 @@
global using NUnit.Framework;
//-----------------------------------------------------------------------
// <copyright file="GlobalUsings.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>
//-----------------------------------------------------------------------
// <auto-generated />
global using NUnit.Framework;

View File

@@ -1,11 +1,27 @@
using System.Security.Cryptography;
using System.Text;
using Konscious.Security.Cryptography;
//-----------------------------------------------------------------------
// <copyright file="Cryptography.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 Cryptography;
public class Cryptography
using System.Security.Cryptography;
using System.Text;
using Konscious.Security.Cryptography;
/// <summary>
/// Cryptography class.
/// </summary>
public static class Cryptography
{
/// <summary>
/// Derive a key used for encryption/decryption based on a user password and system salt.
/// </summary>
/// <param name="password">User password.</param>
/// <param name="salt">The salt to use for the Argon2id hash.</param>
/// <returns>Encryption key as byte array.</returns>
public static byte[] DeriveKeyFromPassword(string password, string salt)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
@@ -16,12 +32,18 @@ public class Cryptography
Salt = saltBytes,
DegreeOfParallelism = 8,
MemorySize = 65536,
Iterations = 4
Iterations = 4,
};
return argon2.GetBytes(32); // Generate a 256-bit key
}
/// <summary>
/// Encrypt a plaintext string using AES-256.
/// </summary>
/// <param name="plaintext">The plaintext string.</param>
/// <param name="key">Key to use for encryption.</param>
/// <returns>The encrypted string (ciphertext).</returns>
public static string Encrypt(string plaintext, byte[] key)
{
using (Aes aes = Aes.Create())
@@ -53,6 +75,12 @@ public class Cryptography
}
}
/// <summary>
/// Decrypt a ciphertext string using AES-256.
/// </summary>
/// <param name="ciphertext">The encrypted string (ciphertext).</param>
/// <param name="key">The key used to originally encrypt the string.</param>
/// <returns>The original plaintext string.</returns>
public static string Decrypt(string ciphertext, byte[] key)
{
byte[] fullCipher = Convert.FromBase64String(ciphertext);

View File

@@ -6,8 +6,25 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\Cryptography.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,70 +1,88 @@
namespace FaviconExtractor;
//-----------------------------------------------------------------------
// <copyright file="FaviconExtractor.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 FaviconExtractor;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using HtmlAgilityPack;
public class FaviconService
/// <summary>
/// Favicon service for extracting favicons from URLs.
/// </summary>
public static class FaviconExtractor
{
public static async Task<byte[]> GetFaviconAsync(string url)
/// <summary>
/// Extracts the favicon from a URL.
/// </summary>
/// <param name="url">The URL to extract the favicon for.</param>
/// <returns>Byte array for favicon image.</returns>
public static async Task<byte[]?> GetFaviconAsync(string url)
{
using (HttpClient client = new HttpClient())
using HttpClient client = new();
HttpResponseMessage response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
HttpResponseMessage response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return null;
}
string htmlContent = await response.Content.ReadAsStringAsync();
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlContent);
// Find all favicon links in the HTML
var faviconNodes = htmlDoc.DocumentNode.SelectNodes("//link[contains(@rel, 'icon')]");
if (faviconNodes == null || !faviconNodes.Any())
{
return null;
}
// Extract favicon URLs and their sizes
var favicons = faviconNodes
.Select(node => new
{
Url = node.GetAttributeValue("href", null),
Size = GetFaviconSize(node.GetAttributeValue("sizes", "0x0"))
})
.Where(favicon => !string.IsNullOrEmpty(favicon.Url))
.OrderByDescending(favicon => favicon.Size)
.ToList();
if (!favicons.Any())
{
return null;
}
var bestFavicon = favicons.First();
var faviconUrl = bestFavicon.Url;
// If the favicon URL is relative, convert it to an absolute URL
if (!Uri.IsWellFormedUriString(faviconUrl, UriKind.Absolute))
{
var baseUri = new Uri(url);
faviconUrl = new Uri(baseUri, faviconUrl).ToString();
}
HttpResponseMessage faviconResponse = await client.GetAsync(faviconUrl);
if (!faviconResponse.IsSuccessStatusCode)
{
return null;
}
byte[] faviconBytes = await faviconResponse.Content.ReadAsByteArrayAsync();
return faviconBytes;
return null;
}
string htmlContent = await response.Content.ReadAsStringAsync();
HtmlDocument htmlDoc = new();
htmlDoc.LoadHtml(htmlContent);
// Find all favicon links in the HTML
var faviconNodes = htmlDoc.DocumentNode.SelectNodes("//link[contains(@rel, 'icon')]");
if (faviconNodes == null || faviconNodes.Count == 0)
{
return null;
}
// Extract favicon URLs and their sizes
var favicons = faviconNodes
.Select(node => new
{
Url = node.GetAttributeValue("href", null),
Size = GetFaviconSize(node.GetAttributeValue("sizes", "0x0")),
})
.Where(favicon => !string.IsNullOrEmpty(favicon.Url))
.OrderByDescending(favicon => favicon.Size)
.ToList();
if (favicons.Count == 0)
{
return null;
}
var bestFavicon = favicons[0];
var faviconUrl = bestFavicon.Url;
// If the favicon URL is relative, convert it to an absolute URL
if (!Uri.IsWellFormedUriString(faviconUrl, UriKind.Absolute))
{
var baseUri = new Uri(url);
faviconUrl = new Uri(baseUri, faviconUrl).ToString();
}
HttpResponseMessage faviconResponse = await client.GetAsync(faviconUrl);
if (!faviconResponse.IsSuccessStatusCode)
{
return null;
}
byte[] faviconBytes = await faviconResponse.Content.ReadAsByteArrayAsync();
return faviconBytes;
}
/// <summary>
/// Gets the size of a favicon from a size string.
/// </summary>
/// <param name="size">Size string.</param>
/// <returns>Int which represent pixel count of image size.</returns>
private static int GetFaviconSize(string size)
{
if (string.IsNullOrEmpty(size) || size == "any")

View File

@@ -6,8 +6,26 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\FaviconExtractor.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\FaviconExtractor.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>