mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-16 12:29:01 -04:00
Merge pull request #12 from lanedirt/11-implement-stylecop-analyzer-and-refactor
Code style refactor
This commit is contained in:
@@ -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
1
.gitignore
vendored
@@ -369,3 +369,4 @@ MigrationBackup/
|
||||
FodyWeavers.xsd
|
||||
|
||||
.idea
|
||||
*.licenseheader
|
||||
|
||||
@@ -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
13
SonarLint.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
// <auto-generated/>
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
// <auto-generated/>
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -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 = ''
|
||||
");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
// <auto-generated/>
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
540
src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.Designer.cs
generated
Normal file
540
src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
38
src/AliasGenerators/Identity/Models/Address.cs
Normal file
38
src/AliasGenerators/Identity/Models/Address.cs
Normal 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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
38
src/AliasGenerators/Identity/Models/Job.cs
Normal file
38
src/AliasGenerators/Identity/Models/Job.cs
Normal 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!;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@code {
|
||||
[Inject] private NavigationManager Navigation { get; set; }
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.NavigateTo("/user/login");
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
179
src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs
Normal file
179
src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/Tests/AliasVault.E2ETests/Common/TestDefaults.cs
Normal file
19
src/Tests/AliasVault.E2ETests/Common/TestDefaults.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user