diff --git a/.editorconfig b/.editorconfig index 0d90013eb..e756cdac4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index 6f9cb9c9e..147b5ae38 100644 --- a/.gitignore +++ b/.gitignore @@ -369,3 +369,4 @@ MigrationBackup/ FodyWeavers.xsd .idea +*.licenseheader diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f31545d31..109800c36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/SonarLint.xml b/SonarLint.xml new file mode 100644 index 000000000..f03773dad --- /dev/null +++ b/SonarLint.xml @@ -0,0 +1,13 @@ + + + + S1135 + + + sonarlint.rule.enabled + false + + + + + diff --git a/aliasvault.sln b/aliasvault.sln index e8177947d..c3f388fd9 100644 --- a/aliasvault.sln +++ b/aliasvault.sln @@ -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 diff --git a/src/AliasDb/AliasDb.csproj b/src/AliasDb/AliasDb.csproj index 7237aabb2..6dee2ea42 100644 --- a/src/AliasDb/AliasDb.csproj +++ b/src/AliasDb/AliasDb.csproj @@ -4,22 +4,31 @@ net8.0 enable enable + True + + + + true + + + + true - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,6 +38,7 @@ LICENSE.md + diff --git a/src/AliasDb/AliasDbContext.cs b/src/AliasDb/AliasDbContext.cs index 086a3e2bc..eac1c9066 100644 --- a/src/AliasDb/AliasDbContext.cs +++ b/src/AliasDb/AliasDbContext.cs @@ -1,8 +1,15 @@ -namespace AliasDb; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasDb; + +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; /// /// The AliasDbContext class. @@ -48,29 +55,7 @@ public class AliasDbContext : IdentityDbContext /// /// Gets or sets the AspNetUserRefreshTokens DbSet. /// - public DbSet AspNetUserRefreshTokens { get; set; } - - /// - /// Sets up the connection string if it is not already configured. - /// - /// DbContextOptionsBuilder instance. - 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 AspNetUserRefreshTokens { get; set; } /// /// 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() + // Configure the User - AspNetUserRefreshToken entity + modelBuilder.Entity() .HasOne(p => p.User) .WithMany() .HasForeignKey(p => p.UserId) .IsRequired(); } + + /// + /// Sets up the connection string if it is not already configured. + /// + /// DbContextOptionsBuilder instance. + 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(); + } + } } diff --git a/src/AliasDb/AspNetUserRefreshTokens.cs b/src/AliasDb/AspNetUserRefreshToken.cs similarity index 75% rename from src/AliasDb/AspNetUserRefreshTokens.cs rename to src/AliasDb/AspNetUserRefreshToken.cs index 09510c5f8..230c9cd6e 100644 --- a/src/AliasDb/AspNetUserRefreshTokens.cs +++ b/src/AliasDb/AspNetUserRefreshToken.cs @@ -1,13 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasDb; -using Microsoft.AspNetCore.Identity; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.AspNetCore.Identity; /// /// Refresh tokens for users. /// -public class AspNetUserRefreshTokens +public class AspNetUserRefreshToken { /// /// Gets or sets Refresh Token ID. diff --git a/src/AliasDb/Identity.cs b/src/AliasDb/Identity.cs index d3adcbb1c..c505ddeca 100644 --- a/src/AliasDb/Identity.cs +++ b/src/AliasDb/Identity.cs @@ -1,3 +1,9 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasDb; using System.ComponentModel.DataAnnotations; @@ -26,14 +32,14 @@ public class Identity /// [StringLength(255)] [Column(TypeName = "VARCHAR")] - public string FirstName { get; set; } = null!; + public string? FirstName { get; set; } = null!; /// /// Gets or sets the last name. /// [StringLength(255)] [Column(TypeName = "VARCHAR")] - public string LastName { get; set; } = null!; + public string? LastName { get; set; } = null!; /// /// Gets or sets the nickname. diff --git a/src/AliasDb/Login.cs b/src/AliasDb/Login.cs index 55c10f318..8695d935c 100644 --- a/src/AliasDb/Login.cs +++ b/src/AliasDb/Login.cs @@ -1,8 +1,14 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasDb; -using Microsoft.AspNetCore.Identity; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.AspNetCore.Identity; /// /// Login object. diff --git a/src/AliasDb/Migrations/20240526135300_InitialMigration.cs b/src/AliasDb/Migrations/20240526135300_InitialMigration.cs index 8dea7095e..545dfcdb5 100644 --- a/src/AliasDb/Migrations/20240526135300_InitialMigration.cs +++ b/src/AliasDb/Migrations/20240526135300_InitialMigration.cs @@ -1,4 +1,5 @@ -using System; +// +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/AliasDb/Migrations/20240527082514_BasicEntities.cs b/src/AliasDb/Migrations/20240527082514_BasicEntities.cs index 872388fd1..877512e87 100644 --- a/src/AliasDb/Migrations/20240527082514_BasicEntities.cs +++ b/src/AliasDb/Migrations/20240527082514_BasicEntities.cs @@ -1,4 +1,5 @@ -using System; +// +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/AliasDb/Migrations/20240529140248_LoginUserId.cs b/src/AliasDb/Migrations/20240529140248_LoginUserId.cs index ce6cdb91e..9872ca0de 100644 --- a/src/AliasDb/Migrations/20240529140248_LoginUserId.cs +++ b/src/AliasDb/Migrations/20240529140248_LoginUserId.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// +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 = '' "); diff --git a/src/AliasDb/Migrations/20240531142952_AddServiceLogo.cs b/src/AliasDb/Migrations/20240531142952_AddServiceLogo.cs index 309c072c8..dc33afc0a 100644 --- a/src/AliasDb/Migrations/20240531142952_AddServiceLogo.cs +++ b/src/AliasDb/Migrations/20240531142952_AddServiceLogo.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/AliasDb/Migrations/20240603175245_AddAspNetUserRefreshTokens.cs b/src/AliasDb/Migrations/20240603175245_AddAspNetUserRefreshTokens.cs index 44cb11021..c8e76f83a 100644 --- a/src/AliasDb/Migrations/20240603175245_AddAspNetUserRefreshTokens.cs +++ b/src/AliasDb/Migrations/20240603175245_AddAspNetUserRefreshTokens.cs @@ -1,4 +1,5 @@ -using System; +// +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.Designer.cs b/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.Designer.cs new file mode 100644 index 000000000..4ed4f6632 --- /dev/null +++ b/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.Designer.cs @@ -0,0 +1,540 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ExpireDate") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRefreshTokens"); + }); + + modelBuilder.Entity("AliasDb.Identity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddressCity") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("AddressCountry") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("AddressState") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("AddressStreet") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("AddressZipCode") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("BankAccountIBAN") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DefaultPasswordId") + .HasColumnType("TEXT"); + + b.Property("EmailPrefix") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("Gender") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("Hobbies") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("NickName") + .HasMaxLength(255) + .HasColumnType("VARCHAR"); + + b.Property("PhoneMobile") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DefaultPasswordId"); + + b.ToTable("Identities"); + }); + + modelBuilder.Entity("AliasDb.Login", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("IdentityId") + .HasColumnType("TEXT"); + + b.Property("ServiceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LoginId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LoginId"); + + b.ToTable("Passwords"); + }); + + modelBuilder.Entity("AliasDb.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Logo") + .HasColumnType("BLOB"); + + b.Property("Name") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("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", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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 + } + } +} diff --git a/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.cs b/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.cs new file mode 100644 index 000000000..097c54973 --- /dev/null +++ b/src/AliasDb/Migrations/20240616200303_ChangeColumnDefinitions.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AliasDb.Migrations +{ + /// + public partial class ChangeColumnDefinitions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Identities", + type: "VARCHAR", + maxLength: 255, + nullable: true, + oldClrType: typeof(string), + oldType: "VARCHAR", + oldMaxLength: 255); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "Identities", + type: "VARCHAR", + maxLength: 255, + nullable: true, + oldClrType: typeof(string), + oldType: "VARCHAR", + oldMaxLength: 255); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Identities", + type: "VARCHAR", + maxLength: 255, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "VARCHAR", + oldMaxLength: 255, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "Identities", + type: "VARCHAR", + maxLength: 255, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "VARCHAR", + oldMaxLength: 255, + oldNullable: true); + } + } +} diff --git a/src/AliasDb/Migrations/AliasDbContextModelSnapshot.cs b/src/AliasDb/Migrations/AliasDbContextModelSnapshot.cs index 9f39aa57c..6abdf3973 100644 --- a/src/AliasDb/Migrations/AliasDbContextModelSnapshot.cs +++ b/src/AliasDb/Migrations/AliasDbContextModelSnapshot.cs @@ -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("Id") .ValueGeneratedOnAdd() @@ -100,7 +100,6 @@ namespace AliasDb.Migrations .HasColumnType("TEXT"); b.Property("FirstName") - .IsRequired() .HasMaxLength(255) .HasColumnType("VARCHAR"); @@ -113,7 +112,6 @@ namespace AliasDb.Migrations .HasColumnType("TEXT"); b.Property("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() diff --git a/src/AliasDb/Password.cs b/src/AliasDb/Password.cs index d24416f5e..7f59460f1 100644 --- a/src/AliasDb/Password.cs +++ b/src/AliasDb/Password.cs @@ -1,3 +1,9 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasDb; using System.ComponentModel.DataAnnotations; diff --git a/src/AliasDb/Service.cs b/src/AliasDb/Service.cs index 17a1037c8..6be0fd790 100644 --- a/src/AliasDb/Service.cs +++ b/src/AliasDb/Service.cs @@ -1,3 +1,9 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasDb; using System.ComponentModel.DataAnnotations; diff --git a/src/AliasGenerators/AliasGenerators.csproj b/src/AliasGenerators/AliasGenerators.csproj index 2be79c532..23b2caeb1 100644 --- a/src/AliasGenerators/AliasGenerators.csproj +++ b/src/AliasGenerators/AliasGenerators.csproj @@ -4,10 +4,27 @@ net8.0 enable enable + True + + + + true + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/AliasGenerators/Identity/IIdentityGenerator.cs b/src/AliasGenerators/Identity/IIdentityGenerator.cs index 7517b3df0..7addd5a15 100644 --- a/src/AliasGenerators/Identity/IIdentityGenerator.cs +++ b/src/AliasGenerators/Identity/IIdentityGenerator.cs @@ -1,6 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Identity; +/// +/// IdentityGenerator interface. +/// public interface IIdentityGenerator { + /// + /// Generates a random identity. + /// + /// Identity model object which contains the random identity. Task GenerateRandomIdentityAsync(); } diff --git a/src/AliasGenerators/Identity/Implementations/FigIdentityGenerator.cs b/src/AliasGenerators/Identity/Implementations/FigIdentityGenerator.cs index 179b39cce..faab122af 100644 --- a/src/AliasGenerators/Identity/Implementations/FigIdentityGenerator.cs +++ b/src/AliasGenerators/Identity/Implementations/FigIdentityGenerator.cs @@ -1,25 +1,38 @@ -using System.Text.Json; - +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Identity.Implementations; +using System.Text.Json; + /// /// Identity generator which generates random identities using the identiteitgenerator.nl semi-public API. /// 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"; + /// public async Task 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(json, new JsonSerializerOptions { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, }); + if (identity is null) + { + throw new InvalidOperationException("Failed to deserialize the identity from FIG WebApi."); + } + return identity; } } diff --git a/src/AliasGenerators/Identity/Implementations/StaticIdentityGenerator.cs b/src/AliasGenerators/Identity/Implementations/StaticIdentityGenerator.cs index 75f0116fe..5b56f52d0 100644 --- a/src/AliasGenerators/Identity/Implementations/StaticIdentityGenerator.cs +++ b/src/AliasGenerators/Identity/Implementations/StaticIdentityGenerator.cs @@ -1,17 +1,23 @@ -using AliasGenerators.Identity; - +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Identity.Implementations; +using AliasGenerators.Identity; + /// /// Static identity generator which implements IIdentityGenerator but always returns /// the same static identity for testing purposes. /// public class StaticIdentityGenerator : IIdentityGenerator { + /// public async Task 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", diff --git a/src/AliasGenerators/Identity/Models/Address.cs b/src/AliasGenerators/Identity/Models/Address.cs new file mode 100644 index 000000000..2cac78a54 --- /dev/null +++ b/src/AliasGenerators/Identity/Models/Address.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasGenerators.Identity.Models; + +/// +/// Address model. +/// +public class Address +{ + /// + /// Gets or sets the street. + /// + public string Street { get; set; } = null!; + + /// + /// Gets or sets the city. + /// + public string City { get; set; } = null!; + + /// + /// Gets or sets the state. + /// + public string State { get; set; } = null!; + + /// + /// Gets or sets the zip code. + /// + public string ZipCode { get; set; } = null!; + + /// + /// Gets or sets the country. + /// + public string Country { get; set; } = null!; +} diff --git a/src/AliasGenerators/Identity/Models/Identity.cs b/src/AliasGenerators/Identity/Models/Identity.cs index deed581e6..15b79fac9 100644 --- a/src/AliasGenerators/Identity/Models/Identity.cs +++ b/src/AliasGenerators/Identity/Models/Identity.cs @@ -1,38 +1,88 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Identity.Models; +/// +/// Identity model. +/// public class Identity { - public string Id { get; set; } + /// + /// Gets or sets the id. + /// + public string Id { get; set; } = null!; + + /// + /// Gets or sets the gender. + /// public int Gender { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string NickName { get; set; } + + /// + /// Gets or sets the first name. + /// + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets the last name. + /// + public string LastName { get; set; } = null!; + + /// + /// Gets or sets the nickname. This is also used as the username. + /// + public string NickName { get; set; } = null!; + + /// + /// Gets or sets the birth date. + /// public DateTime BirthDate { get; set; } - public Address Address { get; set; } - public Job Job { get; set; } - public List 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; } -} + /// + /// Gets or sets the address. + /// + 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; } + /// + /// Gets or sets the job. + /// + public Job Job { get; set; } = null!; + + /// + /// Gets or sets the hobbies. + /// + public List Hobbies { get; set; } = null!; + + /// + /// Gets or sets the email address prefix. + /// + public string EmailPrefix { get; set; } = null!; + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } = null!; + + /// + /// Gets or sets the phone mobile. + /// + public string PhoneMobile { get; set; } = null!; + + /// + /// Gets or sets the bank account IBAN. + /// + public string BankAccountIBAN { get; set; } = null!; + + /// + /// Gets or sets the profile photo in base64 format. + /// + public string ProfilePhotoBase64 { get; set; } = null!; + + /// + /// Gets or sets the profile photo prompt. + /// + public string ProfilePhotoPrompt { get; set; } = null!; } diff --git a/src/AliasGenerators/Identity/Models/Job.cs b/src/AliasGenerators/Identity/Models/Job.cs new file mode 100644 index 000000000..842aaa8cc --- /dev/null +++ b/src/AliasGenerators/Identity/Models/Job.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasGenerators.Identity.Models; + +/// +/// Job model. +/// +public class Job +{ + /// + /// Gets or sets the title. + /// + public string Title { get; set; } = null!; + + /// + /// Gets or sets the company. + /// + public string Company { get; set; } = null!; + + /// + /// Gets or sets the salary. + /// + public string Salary { get; set; } = null!; + + /// + /// Gets or sets the calculated salary. + /// + public decimal SalaryCalculated { get; set; } + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = null!; +} diff --git a/src/AliasGenerators/Password/IPasswordGenerator.cs b/src/AliasGenerators/Password/IPasswordGenerator.cs index f40ffca3a..3f5e5e265 100644 --- a/src/AliasGenerators/Password/IPasswordGenerator.cs +++ b/src/AliasGenerators/Password/IPasswordGenerator.cs @@ -1,6 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Implementations; +/// +/// Interface for password generators. +/// public interface IPasswordGenerator { + /// + /// Generates a random password. + /// + /// Random generated password as string. string GenerateRandomPassword(); } diff --git a/src/AliasGenerators/Password/Implementations/SpamOkPasswordGenerator.cs b/src/AliasGenerators/Password/Implementations/SpamOkPasswordGenerator.cs index db62a9834..a2e127437 100644 --- a/src/AliasGenerators/Password/Implementations/SpamOkPasswordGenerator.cs +++ b/src/AliasGenerators/Password/Implementations/SpamOkPasswordGenerator.cs @@ -1,3 +1,9 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasGenerators.Password.Implementations; using AliasGenerators.Implementations; @@ -7,10 +13,7 @@ using AliasGenerators.Implementations; /// public class SpamOkPasswordGenerator : IPasswordGenerator { - /// - /// Generates a random password using the SpamOK library with diceware (dictionary) method. - /// - /// + /// public string GenerateRandomPassword() { var passwordBuilder = new SpamOK.PasswordGenerator.BasicPasswordBuilder(); @@ -24,7 +27,6 @@ public class SpamOkPasswordGenerator : IPasswordGenerator .GeneratePassword() .ToString(); - return password; } } diff --git a/src/AliasVault.Api/AliasVault.Api.csproj b/src/AliasVault.Api/AliasVault.Api.csproj index fa7343c68..2da78e0e2 100644 --- a/src/AliasVault.Api/AliasVault.Api.csproj +++ b/src/AliasVault.Api/AliasVault.Api.csproj @@ -6,6 +6,15 @@ enable AliasVault.Api Linux + True + + + + true + + + + true @@ -16,7 +25,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -29,6 +42,7 @@ .dockerignore + diff --git a/src/AliasVault.Api/Controllers/AliasController.cs b/src/AliasVault.Api/Controllers/AliasController.cs index 6a15a2d5a..c07e7696a 100644 --- a/src/AliasVault.Api/Controllers/AliasController.cs +++ b/src/AliasVault.Api/Controllers/AliasController.cs @@ -1,22 +1,32 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + 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 +/// +/// Alias controller for handling CRUD operations on the database for alias entities. +/// +/// DbContext instance. +/// UserManager instance. +public class AliasController(AliasDbContext context, UserManager userManager) : AuthenticatedRequestController(userManager) { - private readonly AliasDbContext _context; - - public AliasController(AliasDbContext context, UserManager userManager) : base(userManager) - { - _context = context; - } - + /// + /// Get all alias items for the current user. + /// + /// List of aliases in JSON format. [HttpGet("items")] public async Task 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); } + /// + /// Get a single alias item by its ID. + /// + /// ID of the alias. + /// Alias object as JSON. [HttpGet("{aliasId}")] public async Task 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 } /// - /// Insert a new entry to the database. + /// Insert a new alias to the database. /// - /// - /// + /// Alias model. + /// ID of newly inserted alias. [HttpPut("")] public async Task 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); } /// - /// Update an existing entry in the database. + /// Update an existing alias entry in the database. /// - /// - /// - /// + /// The alias ID to update. + /// Alias model. + /// ID of updated alias entry. [HttpPost("{aliasId}")] - public async Task Update([FromBody] Alias model, Guid aliasId) + public async Task 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); } /// - /// Delete an existing entry from the database. + /// Delete an existing alias entry from the database. /// - /// + /// ID of the alias to delete. + /// HTTP status code. [HttpDelete("{aliasId}")] public async Task 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(); } diff --git a/src/AliasVault.Api/Controllers/AuthController.cs b/src/AliasVault.Api/Controllers/AuthController.cs index 612e0d1ee..d3941e86a 100644 --- a/src/AliasVault.Api/Controllers/AuthController.cs +++ b/src/AliasVault.Api/Controllers/AuthController.cs @@ -1,47 +1,56 @@ -using System.Security.Cryptography; -using AliasDb; -using AliasVault.Shared.Models; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- 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; +/// +/// Auth controller for handling authentication. +/// +/// AliasDbContext instance. +/// UserManager instance. +/// SignInManager instance. +/// IConfiguration instance. [Route("api/[controller]")] [ApiController] -public class AuthController : ControllerBase +public class AuthController(AliasDbContext context, UserManager userManager, SignInManager signInManager, IConfiguration configuration) : ControllerBase { - private readonly AliasDbContext _context; - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly IConfiguration _configuration; - private const string LoginProvider = "AliasVault"; - private const string RefreshToken = "RefreshToken"; - - public AuthController(AliasDbContext context, UserManager userManager, SignInManager signInManager, IConfiguration configuration) - { - _context = context; - _userManager = userManager; - _signInManager = signInManager; - _configuration = configuration; - } - + /// + /// Login endpoint used to process login attempt using credentials. + /// + /// Login model. + /// IActionResult. [HttpPost("login")] public async Task 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(); } + /// + /// Refresh endpoint used to refresh an expired access token using a valid refresh token. + /// + /// Token model. + /// IActionResult. [HttpPost("refresh")] public async Task 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 }); - } + /// + /// Revoke endpoint used to revoke a refresh token. + /// + /// Token model. + /// IActionResult. [HttpPost("revoke")] public async Task 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"); } + /// + /// Register endpoint used to register a new user. + /// + /// Register model. + /// IActionResult. [HttpPost("register")] public async Task 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 { - 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 }; } diff --git a/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs b/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs index 59f84c022..45586e241 100644 --- a/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs +++ b/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs @@ -1,26 +1,32 @@ -using Microsoft.AspNetCore.Authorization; - +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.Api.Controllers; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; +/// +/// Base controller for requests that require authentication. +/// +/// UserManager instance. [Route("api/[controller]")] [ApiController] [Authorize] -public class AuthenticatedRequestController : ControllerBase +public class AuthenticatedRequestController(UserManager userManager) : ControllerBase { - private readonly UserManager _userManager; - - public AuthenticatedRequestController(UserManager userManager) - { - _userManager = userManager; - } - + /// + /// Get the current authenticated user. + /// + /// IdentityUser object for current user. protected async Task 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); } } diff --git a/src/AliasVault.Api/Controllers/IdentityController.cs b/src/AliasVault.Api/Controllers/IdentityController.cs index 05db825af..068fb3f57 100644 --- a/src/AliasVault.Api/Controllers/IdentityController.cs +++ b/src/AliasVault.Api/Controllers/IdentityController.cs @@ -1,21 +1,26 @@ -using AliasGenerators.Identity; -using AliasGenerators.Identity.Implementations; - +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.Api.Controllers; +using AliasGenerators.Identity; +using AliasGenerators.Identity.Implementations; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -public class IdentityController : AuthenticatedRequestController +/// +/// Controller for identity generation. +/// +/// UserManager instance. +public class IdentityController(UserManager userManager) : AuthenticatedRequestController(userManager) { - public IdentityController(UserManager userManager) : base(userManager) - { - } - /// /// Proxies the request to the identity generator to generate a random identity. /// - /// + /// Identity model. [HttpGet("generate")] public async Task Generate() { diff --git a/src/AliasVault.Api/Program.cs b/src/AliasVault.Api/Program.cs index 4da4ef7eb..49b0a5c95 100644 --- a/src/AliasVault.Api/Program.cs +++ b/src/AliasVault.Api/Program.cs @@ -1,3 +1,10 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + 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((container, options) => builder.Services.AddDataProtection(); builder.Services.Configure(options => { - options.TokenLifespan = TimeSpan.FromDays(30); // Set token lifespan for refresh tokens + options.TokenLifespan = TimeSpan.FromDays(30); options.Name = "AliasVault"; }); builder.Services.AddIdentity(options => @@ -57,8 +64,6 @@ builder.Services.AddIdentity(options => options.Tokens.ProviderMap.Add("AliasVault", new TokenProviderDescriptor(typeof(DataProtectorTokenProvider))); }) .AddEntityFrameworkStores() - // Note: The AliasVault token provider is used to generate refresh tokens and is also defined - // in the AuthController. .AddDefaultTokenProviders() .AddTokenProvider>("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() }, - Array.Empty() - }}); + }); }); var app = builder.Build(); @@ -144,25 +152,18 @@ using (var scope = app.Services.CreateScope()) var container = scope.ServiceProvider; var db = container.GetRequiredService(); - db.Database.EnsureCreated(); - - /*if (!db..Any()) - { - try - { - db.Initialize(); - } - catch (Exception ex) - { - var logger = container.GetRequiredService>(); - logger.LogError(ex, "An error occurred seeding the database. Error: {Message}", ex.Message); - } - }*/ + await db.Database.EnsureCreatedAsync(); } -app.Run(); +await app.RunAsync(); -/// -/// For starting the WebAPI project in-memory from E2ETests project. -/// -public class AliasVaultApiProgram { } +namespace AliasVault.Api +{ + /// + /// Explicit program class definition. This is required in order to start the WebAPI project + /// in-memory from E2ETests project via WebApplicationFactory. + /// + public partial class Program + { + } +} diff --git a/src/AliasVault.Shared/AliasVault.Shared.csproj b/src/AliasVault.Shared/AliasVault.Shared.csproj index 3a6353295..d56a6ba10 100644 --- a/src/AliasVault.Shared/AliasVault.Shared.csproj +++ b/src/AliasVault.Shared/AliasVault.Shared.csproj @@ -6,4 +6,25 @@ enable + + bin\Debug\net8.0\AliasVault.Shared.xml + true + + + + bin\Release\net8.0\AliasVault.Shared.xml + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/AliasVault.Shared/Models/LoginModel.cs b/src/AliasVault.Shared/Models/LoginModel.cs index 334b0b932..a6c25e221 100644 --- a/src/AliasVault.Shared/Models/LoginModel.cs +++ b/src/AliasVault.Shared/Models/LoginModel.cs @@ -1,7 +1,24 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models; +/// +/// Login model. +/// public class LoginModel { - public string Email { get; set; } - public string Password { get; set; } + /// + /// Gets or sets the email. + /// + public string Email { get; set; } = null!; + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } = null!; } diff --git a/src/AliasVault.Shared/Models/RegisterModel.cs b/src/AliasVault.Shared/Models/RegisterModel.cs index 3d5850113..719288875 100644 --- a/src/AliasVault.Shared/Models/RegisterModel.cs +++ b/src/AliasVault.Shared/Models/RegisterModel.cs @@ -1,9 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models; +/// +/// Register model. +/// public class RegisterModel { - public string Email { get; set; } - public string Password { get; set; } - public string PasswordConfirm { get; set; } - public bool AcceptTerms { get; set; } + /// + /// Gets or sets the email. + /// + public string Email { get; set; } = null!; + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } = null!; + + /// + /// Gets or sets the password confirmation. + /// + public string PasswordConfirm { get; set; } = null!; + + /// + /// Gets or sets a value indicating whether the terms and conditions are accepted or not. + /// + public bool AcceptTerms { get; set; } = false; } diff --git a/src/AliasVault.Shared/Models/TokenModel.cs b/src/AliasVault.Shared/Models/TokenModel.cs index 6e78a28a0..6777dc850 100644 --- a/src/AliasVault.Shared/Models/TokenModel.cs +++ b/src/AliasVault.Shared/Models/TokenModel.cs @@ -1,12 +1,28 @@ -using System.Text.Json.Serialization; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.Shared.Models; +using System.Text.Json.Serialization; + +/// +/// Token model. +/// public class TokenModel { + /// + /// Gets or sets the token. + /// [JsonPropertyName("token")] - public string Token { get; set; } + public string Token { get; set; } = null!; + /// + /// Gets or sets the refresh token. + /// [JsonPropertyName("refreshToken")] - public string RefreshToken { get; set; } + public string RefreshToken { get; set; } = null!; } diff --git a/src/AliasVault.Shared/Models/WebApi/Alias.cs b/src/AliasVault.Shared/Models/WebApi/Alias.cs index 07eb7bb10..15315256f 100644 --- a/src/AliasVault.Shared/Models/WebApi/Alias.cs +++ b/src/AliasVault.Shared/Models/WebApi/Alias.cs @@ -1,10 +1,39 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models.WebApi; +/// +/// Alias model. +/// public class Alias { - public Service Service { get; set; } - public Identity Identity { get; set; } - public Password Password { get; set; } + /// + /// Gets or sets the Alias Service object. + /// + public Service Service { get; set; } = null!; + + /// + /// Gets or sets the Alias Identity object. + /// + public Identity Identity { get; set; } = null!; + + /// + /// Gets or sets the Alias Password object. + /// + public Password Password { get; set; } = null!; + + /// + /// Gets or sets the Alias CreateDate. + /// public DateTime CreateDate { get; set; } + + /// + /// Gets or sets the Alias LastUpdate. + /// public DateTime LastUpdate { get; set; } } diff --git a/src/AliasVault.Shared/Models/WebApi/AliasListEntry.cs b/src/AliasVault.Shared/Models/WebApi/AliasListEntry.cs index 83065c0df..a1746d91e 100644 --- a/src/AliasVault.Shared/Models/WebApi/AliasListEntry.cs +++ b/src/AliasVault.Shared/Models/WebApi/AliasListEntry.cs @@ -1,9 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models.WebApi; +/// +/// Alias list entry model. This model is used to represent an alias in a list with simplified properties. +/// public class AliasListEntry { + /// + /// Gets or sets the alias id. + /// public Guid Id { get; set; } - public byte[] Logo { get; set; } - public string Service { get; set; } + + /// + /// Gets or sets the alias logo byte array. + /// + public byte[]? Logo { get; set; } + + /// + /// Gets or sets the alias service name. + /// + public string Service { get; set; } = null!; + + /// + /// Gets or sets the alias create date. + /// public DateTime CreateDate { get; set; } } diff --git a/src/AliasVault.Shared/Models/WebApi/Identity.cs b/src/AliasVault.Shared/Models/WebApi/Identity.cs index 7a508cdb7..a610e88ea 100644 --- a/src/AliasVault.Shared/Models/WebApi/Identity.cs +++ b/src/AliasVault.Shared/Models/WebApi/Identity.cs @@ -1,23 +1,104 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models.WebApi; +/// +/// Identity model. +/// public class Identity { + /// + /// Gets or sets the identity id. + /// 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; } + + /// + /// Gets or sets the gender. + /// + public string? Gender { get; set; } + + /// + /// Gets or sets the first name. + /// + public string? FirstName { get; set; } + + /// + /// Gets or sets the last name. + /// + public string? LastName { get; set; } + + /// + /// Gets or sets the nickname. + /// + public string? NickName { get; set; } + + /// + /// Gets or sets the birth date. + /// + public string? BirthDate { get; set; } + + /// + /// Gets or sets the street address. + /// + public string? AddressStreet { get; set; } + + /// + /// Gets or sets the city. + /// + public string? AddressCity { get; set; } + + /// + /// Gets or sets the state. + /// 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; } + + /// + /// Gets or sets the zip code. + /// + public string? AddressZipCode { get; set; } + + /// + /// Gets or sets the country. + /// + public string? AddressCountry { get; set; } + + /// + /// Gets or sets the hobbies. + /// + public string? Hobbies { get; set; } + + /// + /// Gets or sets the email prefix. + /// + public string? EmailPrefix { get; set; } + + /// + /// Gets or sets the mobile phone number. + /// + public string? PhoneMobile { get; set; } + + /// + /// Gets or sets the bank account IBAN. + /// + public string? BankAccountIBAN { get; set; } + + /// + /// Gets or sets the date and time of creation. + /// public DateTime CreatedAt { get; set; } + + /// + /// Gets or sets the date and time of last update. + /// public DateTime UpdatedAt { get; set; } + + /// + /// Gets or sets the default password. + /// public Password? DefaultPassword { get; set; } } diff --git a/src/AliasVault.Shared/Models/WebApi/Password.cs b/src/AliasVault.Shared/Models/WebApi/Password.cs index 114ecb632..a7f38b531 100644 --- a/src/AliasVault.Shared/Models/WebApi/Password.cs +++ b/src/AliasVault.Shared/Models/WebApi/Password.cs @@ -1,9 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models.WebApi; +/// +/// Password model. +/// public class Password { - public string Value { get; set; } + /// + /// Gets or sets the value of the password. + /// + public string Value { get; set; } = null!; + + /// + /// Gets or sets the description of the password. + /// public string? Description { get; set; } + + /// + /// Gets or sets the date and time when the password was created. + /// public DateTime CreatedAt { get; set; } + + /// + /// Gets or sets the date and time when the password was last updated. + /// public DateTime UpdatedAt { get; set; } } diff --git a/src/AliasVault.Shared/Models/WebApi/Service.cs b/src/AliasVault.Shared/Models/WebApi/Service.cs index dd48d1533..15b30d82d 100644 --- a/src/AliasVault.Shared/Models/WebApi/Service.cs +++ b/src/AliasVault.Shared/Models/WebApi/Service.cs @@ -1,11 +1,44 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Shared.Models.WebApi; +/// +/// Service model. +/// public class Service { - public string Name { get; set; } + /// + /// Gets or sets the name of the service. + /// + public string Name { get; set; } = null!; + + /// + /// Gets or sets the description of the service. + /// public string? Description { get; set; } + + /// + /// Gets or sets the URL of the service. + /// public string? Url { get; set; } + + /// + /// Gets or sets the logo URL of the service. + /// public string? LogoUrl { get; set; } + + /// + /// Gets or sets the creation date and time of the service. + /// public DateTime CreatedAt { get; set; } + + /// + /// Gets or sets the last updated date and time of the service. + /// public DateTime UpdatedAt { get; set; } } diff --git a/src/AliasVault.WebApp/AliasVault.WebApp.csproj b/src/AliasVault.WebApp/AliasVault.WebApp.csproj index fe3dc1730..5fb654ef5 100644 --- a/src/AliasVault.WebApp/AliasVault.WebApp.csproj +++ b/src/AliasVault.WebApp/AliasVault.WebApp.csproj @@ -7,13 +7,36 @@ Linux + + bin\Debug\net8.0\AliasVault.WebApp.xml + true + + + + true + bin\Release\net8.0\AliasVault.WebApp.xml + + + + True + + + + True + True + + - + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -23,6 +46,7 @@ .dockerignore + diff --git a/src/AliasVault.WebApp/Auth/Components/InputTextField.razor b/src/AliasVault.WebApp/Auth/Components/InputTextField.razor index 41a9b634f..d0777464d 100644 --- a/src/AliasVault.WebApp/Auth/Components/InputTextField.razor +++ b/src/AliasVault.WebApp/Auth/Components/InputTextField.razor @@ -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 ValueChanged { get; set; } - [Parameter] public Expression> ValueExpression { get; set; } - [Parameter] public string Placeholder { get; set; } - [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } + /// + /// Gets or sets the ID of the input field. + /// + [Parameter] public string Id { get; set; } = null!; + + /// + /// Gets or sets the value of the input field. + /// + [Parameter] public string Value { get; set; } = null!; + + /// + /// Gets or sets the event callback that is triggered when the value changes. + /// + [Parameter] public EventCallback ValueChanged { get; set; } + + /// + /// Gets or sets the expression that identifies the value property. + /// + [Parameter] public Expression> ValueExpression { get; set; } = null!; + + /// + /// Gets or sets the placeholder text for the input field. + /// + [Parameter] public string Placeholder { get; set; } = null!; + + /// + /// Gets or sets additional attributes for the input field. + /// + [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } = new(); } diff --git a/src/AliasVault.WebApp/Auth/Pages/Login.razor b/src/AliasVault.WebApp/Auth/Pages/Login.razor index c84cc7299..31c1f725e 100644 --- a/src/AliasVault.WebApp/Auth/Pages/Login.razor +++ b/src/AliasVault.WebApp/Auth/Pages/Login.razor @@ -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

Sign in to AliasVault @@ -45,7 +46,7 @@ @code { LoginModel user = new LoginModel(); - FullScreenLoadingIndicator loadingIndicator; + FullScreenLoadingIndicator loadingIndicator = new(); protected override async Task OnInitializedAsync() { diff --git a/src/AliasVault.WebApp/Auth/Pages/Register.razor b/src/AliasVault.WebApp/Auth/Pages/Register.razor index eba047427..083bca2fe 100644 --- a/src/AliasVault.WebApp/Auth/Pages/Register.razor +++ b/src/AliasVault.WebApp/Auth/Pages/Register.razor @@ -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

Create a Free Account @@ -61,7 +60,7 @@ @code { RegisterModel user = new RegisterModel(); - FullScreenLoadingIndicator loadingIndicator; + FullScreenLoadingIndicator loadingIndicator = new(); List validationErrors = new List(); 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 Errors { get; set; } - public string TraceId { get; set; } + public Dictionary Errors { get; set; } = new(); + public string TraceId { get; set; } = null!; } } diff --git a/src/AliasVault.WebApp/CustomAuthStateProvider.cs b/src/AliasVault.WebApp/Auth/Providers/AuthStateProvider.cs similarity index 50% rename from src/AliasVault.WebApp/CustomAuthStateProvider.cs rename to src/AliasVault.WebApp/Auth/Providers/AuthStateProvider.cs index 4bac47c95..948a6ea9c 100644 --- a/src/AliasVault.WebApp/CustomAuthStateProvider.cs +++ b/src/AliasVault.WebApp/Auth/Providers/AuthStateProvider.cs @@ -1,24 +1,48 @@ -using AliasVault.WebApp.Auth.Services; -using Blazored.LocalStorage; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- -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 +/// +/// Custom authentication state provider for the application. +/// +public class AuthStateProvider(AuthService authService) : AuthenticationStateProvider { - private readonly AuthService _authService; - - public CustomAuthStateProvider(AuthService authService) + /// + /// Parses the claims from the JWT token. + /// + /// The JWT token. + /// The claims parsed from the JWT token. + public static IEnumerable ParseClaimsFromJwt(string jwt) { - _authService = authService; + var payload = jwt.Split('.')[1]; + var jsonBytes = ParseBase64WithoutPadding(payload); + var keyValuePairs = JsonSerializer.Deserialize>(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)); } + /// + /// Gets the authentication state asynchronously. + /// + /// The authentication state. public override async Task 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 ParseClaimsFromJwt(string jwt) - { - var payload = jwt.Split('.')[1]; - var jsonBytes = ParseBase64WithoutPadding(payload); - var keyValuePairs = JsonSerializer.Deserialize>(jsonBytes); - return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); - } - + /// + /// Parses the base64 string without padding. + /// + /// The base64 string. + /// The byte array parsed from the base64 string. 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); } } diff --git a/src/AliasVault.WebApp/Auth/Services/AliasVaultApiHandlerService.cs b/src/AliasVault.WebApp/Auth/Services/AliasVaultApiHandlerService.cs index bb186841b..bdc0f2848 100644 --- a/src/AliasVault.WebApp/Auth/Services/AliasVaultApiHandlerService.cs +++ b/src/AliasVault.WebApp/Auth/Services/AliasVaultApiHandlerService.cs @@ -1,19 +1,28 @@ -using Microsoft.AspNetCore.Components; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.WebApp.Auth.Services; using System.Net; using System.Net.Http.Headers; +using Microsoft.AspNetCore.Components; -public class AliasVaultApiHandlerService : DelegatingHandler +/// +/// 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. +/// +public class AliasVaultApiHandlerService(IServiceProvider serviceProvider) : DelegatingHandler { - private readonly IServiceProvider _serviceProvider; - - public AliasVaultApiHandlerService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - + /// + /// Override the SendAsync method to add the access token to the request headers. + /// + /// HttpRequestMessage instance. + /// CancellationToken instance. + /// HttpResponseMessage. protected override async Task 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(); + var authService = serviceProvider.GetRequiredService(); 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(); + var navigationManager = serviceProvider.GetRequiredService(); navigationManager.NavigateTo("/user/login"); - } } diff --git a/src/AliasVault.WebApp/Auth/Services/AuthService.cs b/src/AliasVault.WebApp/Auth/Services/AuthService.cs index 7f248d5e2..f4c6a0304 100644 --- a/src/AliasVault.WebApp/Auth/Services/AuthService.cs +++ b/src/AliasVault.WebApp/Auth/Services/AuthService.cs @@ -1,27 +1,43 @@ - -using System.Net.Http.Headers; -using Microsoft.AspNetCore.Components.Authorization; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- 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; +/// +/// This service is responsible for handling authentication-related operations such as refreshing tokens, +/// storing tokens, and revoking tokens. +/// 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; + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + /// The local storage service. public AuthService(HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient; _localStorage = localStorage; } + /// + /// Refreshes the access token asynchronously. + /// + /// The new access token. public async Task 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 } /// - /// Retrieve the stored refresh token (e.g., from local storage or a secure place). + /// Retrieves the stored access token asynchronously. /// - /// + /// The stored access token. public async Task GetAccessTokenAsync() { - return await _localStorage.GetItemAsStringAsync(AccessTokenKey); + return await _localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty; } /// - /// Store the new access token (e.g., in local storage) + /// Stores the new access token asynchronously. /// - /// + /// The new access token. + /// A representing the asynchronous operation. public async Task StoreAccessTokenAsync(string newToken) { await _localStorage.SetItemAsStringAsync(AccessTokenKey, newToken); } /// - /// Retrieve the stored refresh token (e.g., from local storage or a secure place). + /// Retrieves the stored refresh token asynchronously. /// - /// + /// The stored refresh token. public async Task GetRefreshTokenAsync() { - return await _localStorage.GetItemAsStringAsync(RefreshTokenKey); + return await _localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty; } /// - /// Store the new access token (e.g., in local storage). + /// Stores the new refresh token asynchronously. /// - /// + /// The new refresh token. + /// A representing the asynchronous operation. public async Task StoreRefreshTokenAsync(string newToken) { await _localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken); } /// - /// Remove the stored access and refresh tokens, called when logging out. + /// Removes the stored access and refresh tokens asynchronously, called when logging out. /// + /// A representing the asynchronous operation. public async Task RemoveTokensAsync() { await _localStorage.RemoveItemAsync(AccessTokenKey); @@ -111,15 +131,22 @@ public class AuthService } /// - /// Revoke the access and refresh tokens on the server. + /// Revokes the access and refresh tokens on the server asynchronously. /// + /// A representing the asynchronous operation. 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); diff --git a/src/AliasVault.WebApp/Components/Alias/Alias.razor b/src/AliasVault.WebApp/Components/Alias/Alias.razor index ba0d336fc..422287698 100644 --- a/src/AliasVault.WebApp/Components/Alias/Alias.razor +++ b/src/AliasVault.WebApp/Components/Alias/Alias.razor @@ -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; - } - } diff --git a/src/AliasVault.WebApp/Components/CopyPasteFormRow.razor b/src/AliasVault.WebApp/Components/CopyPasteFormRow.razor index 529d5e5ea..205d4b89f 100644 --- a/src/AliasVault.WebApp/Components/CopyPasteFormRow.razor +++ b/src/AliasVault.WebApp/Components/CopyPasteFormRow.razor @@ -16,8 +16,8 @@ @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(); diff --git a/src/AliasVault.WebApp/Components/EditFormRow.razor b/src/AliasVault.WebApp/Components/EditFormRow.razor index be9791857..49a0d0f9f 100644 --- a/src/AliasVault.WebApp/Components/EditFormRow.razor +++ b/src/AliasVault.WebApp/Components/EditFormRow.razor @@ -8,15 +8,29 @@ @code { - [Parameter] public string Label { get; set; } - [Parameter] public string Value { get; set; } - [Parameter] public EventCallback ValueChanged { get; set; } + /// + /// Label for the input field. + /// + [Parameter] + public string Label { get; set; } = "Value"; + + /// + /// Value of the input field. + /// + [Parameter] + public string Value { get; set; } = string.Empty; + + /// + /// Callback that is triggered when the value changes. + /// + [Parameter] + public EventCallback 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); } } diff --git a/src/AliasVault.WebApp/Components/Email/RecentEmails.razor b/src/AliasVault.WebApp/Components/Email/RecentEmails.razor index ab5d8945b..2440e1cb1 100644 --- a/src/AliasVault.WebApp/Components/Email/RecentEmails.razor +++ b/src/AliasVault.WebApp/Components/Email/RecentEmails.razor @@ -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 MailboxEmails { get; set; } = new List(); - 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($"https://api.spamok.com/v2/EmailBox/{EmailPrefix}"); + MailboxApiModel? mailbox = await client.GetFromJsonAsync($"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(); } - } diff --git a/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs b/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs index 01813e22a..bf8cebdc5 100644 --- a/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs +++ b/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs @@ -1,7 +1,24 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.WebApp.Components.Models; +/// +/// Represents a breadcrumb item for the breadcrumb component. +/// public class BreadcrumbItem { + /// + /// Gets or sets the display name for the breadcrumb item. + /// public string? DisplayName { get; set; } + + /// + /// Gets or sets the URL for the breadcrumb item. + /// public string? Url { get; set; } } diff --git a/src/AliasVault.WebApp/Components/RedirectToLogin.razor b/src/AliasVault.WebApp/Components/RedirectToLogin.razor index 145177ab4..885bbb063 100644 --- a/src/AliasVault.WebApp/Components/RedirectToLogin.razor +++ b/src/AliasVault.WebApp/Components/RedirectToLogin.razor @@ -1,6 +1,6 @@ -@code { - [Inject] private NavigationManager Navigation { get; set; } +@inject NavigationManager Navigation +@code { protected override void OnInitialized() { Navigation.NavigateTo("/user/login"); diff --git a/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor b/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor index f6d89937b..aec47cc17 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor +++ b/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor @@ -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 { diff --git a/src/AliasVault.WebApp/Pages/Aliases/Delete.razor b/src/AliasVault.WebApp/Pages/Aliases/Delete.razor index 79ce4bd7c..a2e60d2eb 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/Delete.razor +++ b/src/AliasVault.WebApp/Pages/Aliases/Delete.razor @@ -19,7 +19,7 @@ @if (IsLoading) { - + } 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" }); } diff --git a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/AttachmentApiModel.cs b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/AttachmentApiModel.cs index bc7cc5c17..6200df3cc 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/AttachmentApiModel.cs +++ b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/AttachmentApiModel.cs @@ -1,10 +1,39 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace BlazorServer.Models.Spamok; +/// +/// Represents an attachment for an email. +/// public class AttachmentApiModel { + /// + /// Gets or sets the ID of the attachment. + /// public int Id { get; set; } + + /// + /// Gets or sets the ID of the email the attachment belongs to. + /// public int Email_Id { get; set; } - public string Filename { get; set; } - public string MimeType { get; set; } + + /// + /// Gets or sets the filename of the attachment. + /// + public string Filename { get; set; } = null!; + + /// + /// Gets or sets the MIME type of the attachment. + /// + public string MimeType { get; set; } = null!; + + /// + /// Gets or sets the size of the attachment in bytes. + /// public int Filesize { get; set; } -} \ No newline at end of file +} diff --git a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/EmailApiModel.cs b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/EmailApiModel.cs index 924294851..f19a899c5 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/EmailApiModel.cs +++ b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/EmailApiModel.cs @@ -1,18 +1,79 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace BlazorServer.Models.Spamok; +/// +/// Represents an email API model. +/// public class EmailApiModel { + /// + /// Gets or sets the ID of the email. + /// 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; } + + /// + /// Gets or sets the subject of the email. + /// + public string Subject { get; set; } = string.Empty; + + /// + /// Gets or sets the display name of the sender. + /// + public string FromDisplay { get; set; } = string.Empty; + + /// + /// Gets or sets the domain of the sender's email address. + /// + public string FromDomain { get; set; } = string.Empty; + + /// + /// Gets or sets the local part of the sender's email address. + /// + public string FromLocal { get; set; } = string.Empty; + + /// + /// Gets or sets the domain of the recipient's email address. + /// + public string ToDomain { get; set; } = string.Empty; + + /// + /// Gets or sets the local part of the recipient's email address. + /// + public string ToLocal { get; set; } = string.Empty; + + /// + /// Gets or sets the date of the email. + /// public DateTime Date { get; set; } + + /// + /// Gets or sets the system date of the email. + /// public DateTime DateSystem { get; set; } + + /// + /// Gets or sets the number of seconds ago the email was received. + /// public double SecondsAgo { get; set; } + + /// + /// Gets or sets the HTML content of the email message. + /// public string? MessageHtml { get; set; } + + /// + /// Gets or sets the plain text content of the email message. + /// public string? MessagePlain { get; set; } - public List Attachments { get; set; } -} \ No newline at end of file + + /// + /// Gets or sets the list of attachments in the email. + /// + public List Attachments { get; set; } = new(); +} diff --git a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxApiModel.cs b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxApiModel.cs index 057912510..45ba35741 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxApiModel.cs +++ b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxApiModel.cs @@ -1,8 +1,29 @@ -namespace BlazorServer.Models.Spamok; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasVault.WebApp.Pages.Aliases.Models.Spamok; + +/// +/// Represents a mailbox API model. +/// public class MailboxApiModel { - public string Address { get; set; } + /// + /// Gets or sets the address of the mailbox. + /// + public string Address { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the mailbox is subscribed. + /// public bool Subscribed { get; set; } - public List Mails { get; set; } -} \ No newline at end of file + + /// + /// Gets or sets the list of mailbox email API models. + /// + public List Mails { get; set; } = new(); +} diff --git a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxEmailApiModel.cs b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxEmailApiModel.cs index 7da7477cb..9a40df224 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxEmailApiModel.cs +++ b/src/AliasVault.WebApp/Pages/Aliases/Mailbox/Models/Spamok/MailboxEmailApiModel.cs @@ -1,16 +1,69 @@ -namespace BlazorServer.Models.Spamok; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasVault.WebApp.Pages.Aliases.Models.Spamok; + +/// +/// Represents a mailbox email API model. +/// public class MailboxEmailApiModel { + /// + /// Gets or sets the ID of the email. + /// 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; } + + /// + /// Gets or sets the subject of the email. + /// + public string Subject { get; set; } = string.Empty; + + /// + /// Gets or sets the display name of the sender. + /// + public string FromDisplay { get; set; } = string.Empty; + + /// + /// Gets or sets the domain of the sender's email address. + /// + public string FromDomain { get; set; } = string.Empty; + + /// + /// Gets or sets the local part of the sender's email address. + /// + public string FromLocal { get; set; } = string.Empty; + + /// + /// Gets or sets the domain of the recipient's email address. + /// + public string ToDomain { get; set; } = string.Empty; + + /// + /// Gets or sets the local part of the recipient's email address. + /// + public string ToLocal { get; set; } = string.Empty; + + /// + /// Gets or sets the date of the email. + /// public DateTime Date { get; set; } + + /// + /// Gets or sets the system date of the email. + /// public DateTime DateSystem { get; set; } - public string MessagePreview { get; set; } + + /// + /// Gets or sets the preview of the email message. + /// + public string MessagePreview { get; set; } = string.Empty; + + /// + /// Gets or sets the number of seconds ago the email was received. + /// public double SecondsAgo { get; set; } -} \ No newline at end of file +} diff --git a/src/AliasVault.WebApp/Pages/Aliases/View.razor b/src/AliasVault.WebApp/Pages/Aliases/View.razor index de11a830a..2164fa633 100644 --- a/src/AliasVault.WebApp/Pages/Aliases/View.razor +++ b/src/AliasVault.WebApp/Pages/Aliases/View.razor @@ -64,7 +64,7 @@ else
- +
diff --git a/src/AliasVault.WebApp/Pages/Base/PageBase.cs b/src/AliasVault.WebApp/Pages/Base/PageBase.cs index 043678d27..fe2f2baaa 100644 --- a/src/AliasVault.WebApp/Pages/Base/PageBase.cs +++ b/src/AliasVault.WebApp/Pages/Base/PageBase.cs @@ -1,10 +1,17 @@ -using AliasVault.WebApp.Components.Models; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +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; - /// /// 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; /// public class PageBase : OwningComponentBase { + private bool _parametersInitialSet; + + /// + /// Gets or sets the NavigationManager. + /// [Inject] public NavigationManager NavigationManager { get; set; } = null!; + /// + /// Gets or sets the AuthenticationStateProvider. + /// [Inject] public AuthenticationStateProvider AuthStateProvider { get; set; } = null!; + /// + /// Gets or sets the IJSRuntime. + /// [Inject] public IJSRuntime Js { get; set; } = null!; /// - /// 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. /// protected List BreadcrumbItems { get; set; } = new List(); - private bool _parametersInitialSet; - + /// + /// Initializes the component asynchronously. + /// + /// A representing the asynchronous operation. 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); } /// - /// Get username from the authentication state. + /// Gets the username from the authentication state asynchronously. /// - /// + /// The username. protected async Task GetUsernameAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); return authState.User?.Identity?.Name ?? "[Unknown]"; } + /// + /// Sets the parameters asynchronously. + /// + /// A representing the asynchronous operation. 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; } } } diff --git a/src/AliasVault.WebApp/Pages/Home.razor b/src/AliasVault.WebApp/Pages/Home.razor index fb59f522d..2b86a9826 100644 --- a/src/AliasVault.WebApp/Pages/Home.razor +++ b/src/AliasVault.WebApp/Pages/Home.razor @@ -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(); } diff --git a/src/AliasVault.WebApp/Program.cs b/src/AliasVault.WebApp/Program.cs index 35c7a3b2c..698dc46d4 100644 --- a/src/AliasVault.WebApp/Program.cs +++ b/src/AliasVault.WebApp/Program.cs @@ -1,36 +1,42 @@ -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +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"); builder.RootComponents.Add("head::after"); - -builder.Services.AddTransient(); -builder.Services.AddScoped(); -builder.Services.AddHttpClient("AliasVault.Api") - .AddHttpMessageHandler(); - +builder.Services.AddHttpClient("AliasVault.Api").AddHttpMessageHandler(); builder.Services.AddScoped(sp => { var httpClientFactory = sp.GetRequiredService(); 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(); +builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddAuthorizationCore(); builder.Services.AddBlazoredLocalStorage(); - await builder.Build().RunAsync(); diff --git a/src/AliasVault.WebApp/Services/AliasService.cs b/src/AliasVault.WebApp/Services/AliasService.cs index 9abb10ee8..b7643ce5a 100644 --- a/src/AliasVault.WebApp/Services/AliasService.cs +++ b/src/AliasVault.WebApp/Services/AliasService.cs @@ -1,48 +1,50 @@ -using System.Net.Http.Json; -using AliasDb; -using AliasVault.Shared.Models.WebApi; -using Identity = AliasGenerators.Identity.Models.Identity; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.WebApp.Services; -public class AliasService +using System.Net.Http.Json; +using AliasVault.Shared.Models.WebApi; +using Identity = AliasGenerators.Identity.Models.Identity; + +/// +/// Service class for alias operations. +/// +public class AliasService(HttpClient httpClient) { - private HttpClient _httpClient; - - /// - /// Public constructor which can be called from static async method or directly. - /// - /// - /// - public AliasService(HttpClient httpClient) - { - _httpClient = httpClient; - } - /// /// Generate random identity by calling the IdentityGenerator API. /// - /// + /// Identity object. public async Task GenerateRandomIdentityAsync() { - return await _httpClient.GetFromJsonAsync("api/Identity/generate"); + var identity = await httpClient.GetFromJsonAsync("api/Identity/generate"); + if (identity == null) + { + throw new InvalidOperationException("Failed to generate random identity."); + } + + return identity; } /// /// Insert new entry into database. /// - /// + /// Alias object to insert. + /// Guid of inserted entry. public async Task InsertAliasAsync(Alias aliasObject) { - // Put to webapi. try { - var returnObject = await _httpClient.PutAsJsonAsync("api/Alias", aliasObject); + var returnObject = await httpClient.PutAsJsonAsync("api/Alias", aliasObject); return await returnObject.Content.ReadFromJsonAsync(); } 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 /// /// Update an existing entry to database. /// - /// - /// + /// Alias object to update. + /// Id of alias to update. + /// Guid of updated entry. public async Task UpdateAliasAsync(Alias aliasObject, Guid id) { - // Post to webapi. try { - var returnObject = await _httpClient.PostAsJsonAsync("api/Alias/" + id, aliasObject); + var returnObject = await httpClient.PostAsJsonAsync("api/Alias/" + id, aliasObject); return await returnObject.Content.ReadFromJsonAsync(); } 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 /// /// Load existing entry from database. /// - /// + /// Id of alias to load. + /// Alias object. public async Task LoadAliasAsync(Guid aliasId) { - // Make webapi call to get list of all entries. try { - return await _httpClient.GetFromJsonAsync("api/Alias/" + aliasId); + return await httpClient.GetFromJsonAsync("api/Alias/" + aliasId); } catch { - // Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page. return null; } } /// - /// Get list with all entries from database. + /// Get list with all entries that belong to current user. /// + /// List of AliasListEntry objects. public async Task?> GetListAsync() { - // Make webapi call to get list of all entries. try { - return await _httpClient.GetFromJsonAsync>("api/Alias/items"); + return await httpClient.GetFromJsonAsync>("api/Alias/items"); } catch { - // Return null if failed. If authentication failed, the AliasVaultApiHandlerService will redirect to login page. - return null; + return new List(); } } /// /// Removes existing entry from database. /// - /// - public async Task DeleteAliasAsync(Guid Id) + /// Id of alias to delete. + /// Task. + public async Task DeleteAliasAsync(Guid id) { // Delete from webapi. try { - await _httpClient.DeleteAsync("api/Alias/" + Id); + await httpClient.DeleteAsync("api/Alias/" + id); } catch { diff --git a/src/AliasVault.WebApp/Services/ClipboardCopyService.cs b/src/AliasVault.WebApp/Services/ClipboardCopyService.cs index fc0613d65..bd98d854a 100644 --- a/src/AliasVault.WebApp/Services/ClipboardCopyService.cs +++ b/src/AliasVault.WebApp/Services/ClipboardCopyService.cs @@ -1,3 +1,10 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.WebApp.Services; /// @@ -5,18 +12,26 @@ namespace AliasVault.WebApp.Services; /// public class ClipboardCopyService { - private string _currentCopiedId; - public event Action OnCopy; + private string _currentCopiedId = string.Empty; + + /// + /// Event to notify the application that an item has been copied. + /// + public event Action OnCopy = null!; /// /// Keep track of the last copied item. /// - /// + /// Id of the last copied item. public void SetCopied(string id) { _currentCopiedId = id; OnCopy?.Invoke(_currentCopiedId); } + /// + /// Get the last copied item. + /// + /// Id of last copied item. public string GetCopiedId() => _currentCopiedId; } diff --git a/src/AliasVault/AliasVault.csproj b/src/AliasVault/AliasVault.csproj index ee73b6b27..3afae1723 100644 --- a/src/AliasVault/AliasVault.csproj +++ b/src/AliasVault/AliasVault.csproj @@ -6,6 +6,16 @@ enable + + bin\Debug\net8.0\AliasVault.xml + false + + + + bin\Release\net8.0\AliasVault.xml + false + + @@ -16,18 +26,15 @@ LICENSE.md + - - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AliasVault/Components/Pages/Aliases/AddEdit.razor b/src/AliasVault/Components/Pages/Aliases/AddEdit.razor index 95220f0ae..b89706461 100644 --- a/src/AliasVault/Components/Pages/Aliases/AddEdit.razor +++ b/src/AliasVault/Components/Pages/Aliases/AddEdit.razor @@ -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) diff --git a/src/AliasVault/stylecop.json b/src/AliasVault/stylecop.json deleted file mode 100644 index 067679031..000000000 --- a/src/AliasVault/stylecop.json +++ /dev/null @@ -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 - } - } -} diff --git a/src/Tests/AliasVault.E2ETests/AliasTests.cs b/src/Tests/AliasVault.E2ETests/AliasTests.cs index 71ae48593..a911a953c 100644 --- a/src/Tests/AliasVault.E2ETests/AliasTests.cs +++ b/src/Tests/AliasVault.E2ETests/AliasTests.cs @@ -1,71 +1,26 @@ -using Microsoft.Playwright; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.E2ETests; +/// +/// End-to-end tests for the alias management. +/// [Parallelizable(ParallelScope.Self)] [TestFixture] public class AliasTests : PlaywrightTest { - /// - /// Test if registering a new account works. - /// - [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(); /// - /// Test if registering a new account works. + /// Helper method to fill all input fields on a page with random data. /// - [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(); - + /// IPage instance where to fill the input fields for. + /// Async task. 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); } } + /// + /// Test if the alias listing index page works. + /// + /// Async task. + [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."); + } + + /// + /// Test if creating a new alias works. + /// + /// Async task. + [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"; diff --git a/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj b/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj index 3da7ac303..92f7ea3c1 100644 --- a/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj +++ b/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj @@ -9,14 +9,38 @@ true + + true + bin\Debug\net8.0\AliasVault.E2ETests.xml + + + + true + bin\Release\net8.0\AliasVault.E2ETests.xml + + + + + + - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Tests/AliasVault.E2ETests/AuthTests.cs b/src/Tests/AliasVault.E2ETests/AuthTests.cs index cfaaa2a30..240d8c3de 100644 --- a/src/Tests/AliasVault.E2ETests/AuthTests.cs +++ b/src/Tests/AliasVault.E2ETests/AuthTests.cs @@ -1,7 +1,17 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + using Microsoft.Playwright; namespace AliasVault.E2ETests; +/// +/// End-to-end tests for authentication. +/// [Parallelizable(ParallelScope.Self)] [TestFixture] public class AuthTests : PlaywrightTest @@ -9,20 +19,19 @@ public class AuthTests : PlaywrightTest /// /// Test if registering a new account works. /// + /// Async task. [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 /// /// Test if logging in works. /// + /// Async task. 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"); diff --git a/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs new file mode 100644 index 000000000..6cf0f0481 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.E2ETests.Common; + +using Microsoft.Playwright; + +/// +/// Base class for tests that use Playwright for E2E browser testing. +/// +public class PlaywrightTest +{ + /// + /// For starting the WebAPI project in-memory. + /// + private readonly WebApplicationFactoryFixture _factory = new(); + + /// + /// The BlazorWasmAppManager instance. + /// + private BlazorWasmAppManager _blazorWasmAppManager; + + /// + /// Gets or sets base URL where the Blazor WASM app runs on including random port. + /// + protected string AppBaseUrl { get; set; } = string.Empty; + + /// + /// Gets or sets random unique account email that is used for the test. + /// + protected string TestUserEmail { get; set; } = string.Empty; + + /// + /// Gets or sets random unique account password that is used for the test. + /// + protected string TestUserPassword { get; set; } = string.Empty; + + /// + /// Gets the Playwright browser instance. + /// + protected IBrowser Browser { get; private set; } + + /// + /// Gets the Playwright browser context. + /// + protected IBrowserContext Context { get; private set; } + + /// + /// Gets the Playwright page. + /// + protected IPage Page { get; private set; } + + /// + /// One time setup for the Playwright test which runs before all tests in the class. + /// + /// Async task. + [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(); + } + + /// + /// Tear down the Playwright test which runs after all tests are done in the class. + /// + /// Async task. + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await Page.CloseAsync(); + await Context.CloseAsync(); + await Browser.CloseAsync(); + + await _factory.DisposeAsync(); + _blazorWasmAppManager.StopBlazorWasm(); + } + + /// + /// Wait for the specified URL to be loaded with a default timeout. + /// + /// The URL to wait for. This may also contains wildcard such as "**/user/login". + /// Async task. + protected async Task WaitForURLAsync(string url) + { + await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = TestDefaults.DefaultTimeout }); + } + + /// + /// Wait for the specified URL to be loaded with a custom timeout. + /// + /// The URL to wait for. This may also contains wildcard such as "**/user/login". + /// Custom timeout in milliseconds. + /// Async task. + protected async Task WaitForURLAsync(string url, int timeoutInMs) + { + await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = timeoutInMs }); + } + + /// + /// Register a new random account. + /// + /// Async task. + 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); + } +} diff --git a/src/Tests/AliasVault.E2ETests/Common/TestDefaults.cs b/src/Tests/AliasVault.E2ETests/Common/TestDefaults.cs new file mode 100644 index 000000000..5bf285373 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Common/TestDefaults.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.E2ETests.Common; + +/// +/// Default values for tests. +/// +public static class TestDefaults +{ + /// + /// Gets or sets default timeout while waiting for pages to load in milliseconds. + /// + public static int DefaultTimeout { get; set; } = 5000; +} diff --git a/src/Tests/AliasVault.E2ETests/GlobalUsings.cs b/src/Tests/AliasVault.E2ETests/GlobalUsings.cs index a36faac71..46f3d3f5f 100644 --- a/src/Tests/AliasVault.E2ETests/GlobalUsings.cs +++ b/src/Tests/AliasVault.E2ETests/GlobalUsings.cs @@ -1,2 +1,13 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +// global using System.Threading.Tasks; global using NUnit.Framework; +global using AliasVault.E2ETests.Infrastructure; +global using AliasVault.E2ETests.Common; +global using Microsoft.Playwright; diff --git a/src/Tests/AliasVault.E2ETests/WebAppManager.cs b/src/Tests/AliasVault.E2ETests/Infrastructure/BlazorWasmAppManager.cs similarity index 69% rename from src/Tests/AliasVault.E2ETests/WebAppManager.cs rename to src/Tests/AliasVault.E2ETests/Infrastructure/BlazorWasmAppManager.cs index 0cb1c5f01..75c213c14 100644 --- a/src/Tests/AliasVault.E2ETests/WebAppManager.cs +++ b/src/Tests/AliasVault.E2ETests/Infrastructure/BlazorWasmAppManager.cs @@ -1,21 +1,27 @@ -namespace AliasVault.E2ETests; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- +namespace AliasVault.E2ETests.Infrastructure; using System.Diagnostics; using System.Net; -public class WebAppManager +/// +/// A class for managing the Blazor WebAssembly application in out-of-process mode for E2E testing. +/// +public class BlazorWasmAppManager { - private Process _blazorWasmProcess; - private List _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 _blazorWasmErrors = []; + private Process? _blazorWasmProcess; + /// + /// Starts the Blazor WebAssembly application in out-of-process mode. + /// + /// The port number to run the app under. + /// Async task. 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); } + /// + /// Stops the Blazor WebAssembly application process. + /// + 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 diff --git a/src/Tests/AliasVault.E2ETests/WebApplicationFactoryFixture.cs b/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationFactoryFixture.cs similarity index 57% rename from src/Tests/AliasVault.E2ETests/WebApplicationFactoryFixture.cs rename to src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationFactoryFixture.cs index 36cc5d086..d4207e0d1 100644 --- a/src/Tests/AliasVault.E2ETests/WebApplicationFactoryFixture.cs +++ b/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationFactoryFixture.cs @@ -1,3 +1,12 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +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; - +/// +/// Web application factory fixture for integration tests. +/// +/// The entry point. public class WebApplicationFactoryFixture : WebApplicationFactory where TEntryPoint : class { - public string HostUrl { get; set; } = "https://localhost:5001"; // we can use any free port + /// + /// Gets or sets the URL the web application host will listen on. + /// + public string HostUrl { get; set; } = "https://localhost:5001"; + /// 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)); + if (dbContextDescriptor is null) + { + throw new InvalidOperationException( + "No DbContextOptions 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 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(container => { var connection = new SqliteConnection("DataSource=:memory:"); @@ -49,6 +78,7 @@ public class WebApplicationFactoryFixture : WebApplicationFactory protected override IHost CreateHost(IHostBuilder builder) { var dummyHost = builder.Build(); diff --git a/src/Tests/AliasVault.E2ETests/PlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/PlaywrightTest.cs deleted file mode 100644 index e141633c8..000000000 --- a/src/Tests/AliasVault.E2ETests/PlaywrightTest.cs +++ /dev/null @@ -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/"; - - /// - /// For starting the WebAPI project in-memory. - /// - private WebApplicationFactoryFixture _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(); - } - - /// - /// Register a new random account. - /// - 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)); - } -} diff --git a/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj b/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj index b30347e1c..82bdbe9d2 100644 --- a/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj +++ b/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,17 +10,43 @@ AliasVault.Tests + + true + bin\Debug\net8.0\AliasVault.UnitTests.xml + + + + true + bin\Release\net8.0\AliasVault.UnitTests.xml + + - - - - - + + stylecop.json + - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/src/Tests/AliasVault.UnitTests/CryptographyTests.cs b/src/Tests/AliasVault.UnitTests/CryptographyTests.cs index 7854b33ec..f2cdddc8f 100644 --- a/src/Tests/AliasVault.UnitTests/CryptographyTests.cs +++ b/src/Tests/AliasVault.UnitTests/CryptographyTests.cs @@ -1,9 +1,22 @@ -using System.Security.Cryptography; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace AliasVault.Tests; +using System.Security.Cryptography; + +/// +/// Tests for the Cryptography class. +/// public class CryptographyTests { + /// + /// Common setup for all tests. + /// [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); diff --git a/src/Tests/AliasVault.UnitTests/FaviconExtractorTests.cs b/src/Tests/AliasVault.UnitTests/FaviconExtractorTests.cs index f57d7b815..3084839b5 100644 --- a/src/Tests/AliasVault.UnitTests/FaviconExtractorTests.cs +++ b/src/Tests/AliasVault.UnitTests/FaviconExtractorTests.cs @@ -1,16 +1,32 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + namespace AliasVault.Tests; +/// +/// Tests for the FaviconExtractor class. +/// public class FaviconExtractorTests { + /// + /// Common setup for all tests. + /// [SetUp] public void Setup() { } + /// + /// Test extracting a favicon from a known website. + /// [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); } } diff --git a/src/Tests/AliasVault.UnitTests/GlobalUsings.cs b/src/Tests/AliasVault.UnitTests/GlobalUsings.cs index cefced496..7655a4f02 100644 --- a/src/Tests/AliasVault.UnitTests/GlobalUsings.cs +++ b/src/Tests/AliasVault.UnitTests/GlobalUsings.cs @@ -1 +1,10 @@ -global using NUnit.Framework; \ No newline at end of file +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +// + +global using NUnit.Framework; diff --git a/src/Utilities/Cryptography/Cryptography.cs b/src/Utilities/Cryptography/Cryptography.cs index 29335af23..b618c69b1 100644 --- a/src/Utilities/Cryptography/Cryptography.cs +++ b/src/Utilities/Cryptography/Cryptography.cs @@ -1,11 +1,27 @@ -using System.Security.Cryptography; -using System.Text; -using Konscious.Security.Cryptography; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- namespace Cryptography; -public class Cryptography +using System.Security.Cryptography; +using System.Text; +using Konscious.Security.Cryptography; + +/// +/// Cryptography class. +/// +public static class Cryptography { + /// + /// Derive a key used for encryption/decryption based on a user password and system salt. + /// + /// User password. + /// The salt to use for the Argon2id hash. + /// Encryption key as byte array. 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 } + /// + /// Encrypt a plaintext string using AES-256. + /// + /// The plaintext string. + /// Key to use for encryption. + /// The encrypted string (ciphertext). public static string Encrypt(string plaintext, byte[] key) { using (Aes aes = Aes.Create()) @@ -53,6 +75,12 @@ public class Cryptography } } + /// + /// Decrypt a ciphertext string using AES-256. + /// + /// The encrypted string (ciphertext). + /// The key used to originally encrypt the string. + /// The original plaintext string. public static string Decrypt(string ciphertext, byte[] key) { byte[] fullCipher = Convert.FromBase64String(ciphertext); diff --git a/src/Utilities/Cryptography/Cryptography.csproj b/src/Utilities/Cryptography/Cryptography.csproj index d6ede0c2c..3bfe819a1 100644 --- a/src/Utilities/Cryptography/Cryptography.csproj +++ b/src/Utilities/Cryptography/Cryptography.csproj @@ -6,8 +6,25 @@ enable + + true + bin\Debug\net8.0\Cryptography.xml + + + + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Utilities/FaviconExtractor/FaviconExtractor.cs b/src/Utilities/FaviconExtractor/FaviconExtractor.cs index a140aa5fe..b3e72a8df 100644 --- a/src/Utilities/FaviconExtractor/FaviconExtractor.cs +++ b/src/Utilities/FaviconExtractor/FaviconExtractor.cs @@ -1,70 +1,88 @@ -namespace FaviconExtractor; +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace FaviconExtractor; using System; using System.Net.Http; using System.Threading.Tasks; using HtmlAgilityPack; -public class FaviconService +/// +/// Favicon service for extracting favicons from URLs. +/// +public static class FaviconExtractor { - public static async Task GetFaviconAsync(string url) + /// + /// Extracts the favicon from a URL. + /// + /// The URL to extract the favicon for. + /// Byte array for favicon image. + public static async Task 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; } + /// + /// Gets the size of a favicon from a size string. + /// + /// Size string. + /// Int which represent pixel count of image size. private static int GetFaviconSize(string size) { if (string.IsNullOrEmpty(size) || size == "any") diff --git a/src/Utilities/FaviconExtractor/FaviconExtractor.csproj b/src/Utilities/FaviconExtractor/FaviconExtractor.csproj index d6ccd0f77..34516a33b 100644 --- a/src/Utilities/FaviconExtractor/FaviconExtractor.csproj +++ b/src/Utilities/FaviconExtractor/FaviconExtractor.csproj @@ -6,8 +6,26 @@ enable + + true + bin\Debug\net8.0\FaviconExtractor.xml + + + + true + bin\Release\net8.0\FaviconExtractor.xml + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/AliasDb/stylecop.json b/src/stylecop.json similarity index 100% rename from src/AliasDb/stylecop.json rename to src/stylecop.json