diff --git a/CleanAspire.slnx b/CleanAspire.slnx index ef1a0ef..6e93a1d 100644 --- a/CleanAspire.slnx +++ b/CleanAspire.slnx @@ -13,7 +13,7 @@ - + diff --git a/README.md b/README.md index 1d6dea6..8749838 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to version: '3.8' services: apiservice: - image: blazordevlab/cleanaspire-api:0.0.56 + image: blazordevlab/cleanaspire-api:0.0.57 environment: - ASPNETCORE_ENVIRONMENT=Development - AllowedHosts=* @@ -110,7 +110,7 @@ services: blazorweb: - image: blazordevlab/cleanaspire-webapp:0.0.56 + image: blazordevlab/cleanaspire-webapp:0.0.57 environment: - ASPNETCORE_ENVIRONMENT=Production - AllowedHosts=* diff --git a/src/CleanAspire.Api/CleanAspire.Api.csproj b/src/CleanAspire.Api/CleanAspire.Api.csproj index 0ce8ce7..4d1d92f 100644 --- a/src/CleanAspire.Api/CleanAspire.Api.csproj +++ b/src/CleanAspire.Api/CleanAspire.Api.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs index 30d9194..1f0f8af 100644 --- a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs +++ b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Identity.Data; using Google.Apis.Auth; using CleanAspire.Infrastructure.Persistence; +using System.Globalization; namespace CleanAspire.Api; public static class IdentityApiAdditionalEndpointsExtensions @@ -165,7 +166,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(thi .WithDescription("Allows a new user to sign up by providing required details such as email, password, and tenant-specific information. This endpoint creates a new user account and sends a confirmation email for verification."); routeGroup.MapDelete("/deleteOwnerAccount", async Task> - (ClaimsPrincipal claimsPrincipal, SignInManager signInManager, HttpContext context,[FromBody] DeleteUserRequest request) => + (ClaimsPrincipal claimsPrincipal, SignInManager signInManager, HttpContext context, [FromBody] DeleteUserRequest request) => { var userManager = context.RequestServices.GetRequiredService>(); if (await userManager.GetUserAsync(claimsPrincipal) is not { } user) @@ -421,6 +422,187 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(thi .WithSummary("External Login with Google OAuth") .WithDescription("Handles external login using Google OAuth 2.0. Exchanges an authorization code for tokens, validates the user's identity, and signs the user in."); + + routeGroup.MapGet("/generateAuthenticator", async Task, ValidationProblem, NotFound>> + (ClaimsPrincipal claimsPrincipal, HttpContext context, [FromQuery] string appName) => + { + var userManager = context.RequestServices.GetRequiredService>(); + var urlEncoder = context.RequestServices.GetRequiredService(); + if (await userManager.GetUserAsync(claimsPrincipal) is not { } user) + { + return TypedResults.NotFound(); + } + if (string.IsNullOrEmpty(appName)) appName = "Blazor Aspire"; + var unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); + } + var sharedKey = FormatKey(unformattedKey!); + + var email = await userManager.GetEmailAsync(user); + var authenticatorUri = string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + urlEncoder.Encode(appName), + urlEncoder.Encode(email!), + unformattedKey); + return TypedResults.Ok(new AuthenticatorResponse(sharedKey, authenticatorUri)); + }).RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithSummary("Generate an Authenticator URI and shared key") + .WithDescription("Generates a shared key and an Authenticator URI for a logged-in user. This endpoint is typically used to configure a TOTP authenticator app, such as Microsoft Authenticator or Google Authenticator."); + + + routeGroup.MapPost("/enable2fa", async Task>> + (ClaimsPrincipal claimsPrincipal, HttpContext context, [FromBody] Enable2faRequest request) => + { + var userManager = context.RequestServices.GetRequiredService>(); + var urlEncoder = context.RequestServices.GetRequiredService(); + var user = await userManager.GetUserAsync(claimsPrincipal); + if (user is null) + { + return TypedResults.NotFound(); + } + var unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); + } + var sharedKey = FormatKey(unformattedKey!); + var email = await userManager.GetEmailAsync(user); + var authenticatorUri = string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + urlEncoder.Encode(request.AppName ?? "Blazor Aspire"), // 使用用户提供的 appName 或默认值 + urlEncoder.Encode(email!), + unformattedKey); + + var isValid = await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, request.VerificationCode); + if (isValid) + { + await userManager.SetTwoFactorEnabledAsync(user, true); + logger.LogInformation("User has enabled 2fa."); + return TypedResults.Ok(); + } + else + { + return TypedResults.BadRequest("Invalid verification code"); + } + }).RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Enable Authenticator for the user") + .WithDescription("This endpoint enables Two-Factor Authentication (TOTP) for a logged-in user. The user must first scan the provided QR code using an authenticator app, and then verify the generated code to complete the process."); + + routeGroup.MapGet("/disable2fa", async Task> + (ClaimsPrincipal claimsPrincipal, HttpContext context) => + { + var userManager = context.RequestServices.GetRequiredService>(); + var logger = context.RequestServices.GetRequiredService().CreateLogger("Disable2FA"); + var user = await userManager.GetUserAsync(claimsPrincipal); + if (user is null) + { + return TypedResults.NotFound(); + } + var isTwoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + return TypedResults.BadRequest(); + } + var result = await userManager.SetTwoFactorEnabledAsync(user, false); + if (!result.Succeeded) + { + logger.LogError("Failed to disable 2FA"); + return TypedResults.BadRequest(); + } + + logger.LogInformation("User has disabled 2FA."); + return TypedResults.Ok(); + }) + .RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Disable Two-Factor Authentication for the user") + .WithDescription("This endpoint disables Two-Factor Authentication (TOTP) for a logged-in user. The user must already have 2FA enabled for this operation to be valid."); + + routeGroup.MapPost("/login2fa", async Task> + ([FromBody] LoginRequest login, [FromQuery] bool? useCookies, [FromQuery] bool? useSessionCookies, HttpContext context) => + { + var signInManager = context.RequestServices.GetRequiredService>(); + var userManager = context.RequestServices.GetRequiredService>(); + var useCookieScheme = (useCookies == true) || (useSessionCookies == true); + var isPersistent = (useCookies == true) && (useSessionCookies != true); + signInManager.AuthenticationScheme = useCookieScheme ? IdentityConstants.ApplicationScheme : IdentityConstants.BearerScheme; + + var user = await userManager.FindByNameAsync(login.Email); + if (user == null) + { + return TypedResults.NotFound(); + } + + var result = await signInManager.PasswordSignInAsync(login.Email, login.Password, isPersistent, lockoutOnFailure: true); + + if (result.RequiresTwoFactor) + { + if (!string.IsNullOrEmpty(login.TwoFactorCode)) + { + result = await signInManager.TwoFactorAuthenticatorSignInAsync(login.TwoFactorCode, isPersistent, rememberClient: isPersistent); + } + else if (!string.IsNullOrEmpty(login.TwoFactorRecoveryCode)) + { + result = await signInManager.TwoFactorRecoveryCodeSignInAsync(login.TwoFactorRecoveryCode); + } + } + + if (!result.Succeeded) + { + return TypedResults.Problem(result.ToString(), statusCode: StatusCodes.Status401Unauthorized); + } + + // The signInManager already produced the needed response in the form of a cookie or bearer token. + return TypedResults.Ok(); + }).AllowAnonymous() + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Login with optional two-factor authentication") + .WithDescription("This endpoint allows users to log in with their email and password. If two-factor authentication is enabled, the user must provide a valid two-factor code or recovery code. Supports persistent cookies or bearer tokens."); + + + routeGroup.MapGet("generateRecoveryCodes", async Task, NotFound, BadRequest>> + (ClaimsPrincipal claimsPrincipal, HttpContext context) => + { + var userManager = context.RequestServices.GetRequiredService>(); + var logger = context.RequestServices.GetRequiredService().CreateLogger("Disable2FA"); + var user = await userManager.GetUserAsync(claimsPrincipal); + if (user is null) + { + return TypedResults.NotFound(); + } + var isTwoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + return TypedResults.BadRequest(); + } + int codeCount = 8; + var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, codeCount); + return TypedResults.Ok(new RecoveryCodesResponse(recoveryCodes)); + }).RequireAuthorization() + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status400BadRequest) + .WithSummary("Generate recovery codes for two-factor authentication.") + .WithDescription("Generates new recovery codes if two-factor authentication is enabled. " + + "Returns 404 if the user is not found or 400 if 2FA is not enabled."); + async Task SendConfirmationEmailAsync(TUser user, UserManager userManager, HttpContext context, string email, bool isChange = false) { var configuration = context.RequestServices.GetRequiredService(); @@ -472,12 +654,14 @@ async Task SendConfirmationEmailAsync(TUser user, UserManager userManager .WithSummary("Request a password reset link") .WithDescription("Generates and sends a password reset link to the user's email if the email is registered and confirmed."); return endpoints; + } private static async Task CreateInfoResponseAsync(TUser user, UserManager userManager) where TUser : class { if (user is not ApplicationUser appUser) throw new InvalidCastException($"The provided user must be of type {nameof(ApplicationUser)}."); + var isTwoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user); return new() { UserId = await userManager.GetUserIdAsync(user) ?? throw new NotSupportedException("Users must have an ID."), @@ -490,7 +674,8 @@ private static async Task CreateInfoResponseAsync(TUser Provider = appUser.Provider, SuperiorId = appUser.SuperiorId, TimeZoneId = appUser.TimeZoneId, - AvatarUrl = appUser.AvatarUrl + AvatarUrl = appUser.AvatarUrl, + IsTwoFactorEnabled = isTwoFactorEnabled }; } @@ -624,6 +809,25 @@ static string GenerateChangeEmailContent(string confirmEmailUrl) "; } + + private static string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + } public class UpdateEmailRequest @@ -689,6 +893,7 @@ public sealed class ProfileResponse public string? TimeZoneId { get; init; } public string? LanguageCode { get; init; } public string? SuperiorId { get; init; } + public bool IsTwoFactorEnabled { get; init; } } public sealed class SignupRequest { @@ -743,3 +948,15 @@ internal sealed record GoogleAuthResponse( string Email, string? ProfilePicture ); + +internal sealed record AuthenticatorResponse( + string SharedKey, + string AuthenticatorUri +); +internal sealed record Enable2faRequest( + string? AppName, + string VerificationCode + ); +internal sealed record RecoveryCodesResponse( + IEnumerable Codes + ); diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj index 3d5ae2c..0722c3d 100644 --- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj +++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj @@ -31,7 +31,8 @@ - + + diff --git a/src/CleanAspire.ClientApp/Client/.kiota/workspace.json b/src/CleanAspire.ClientApp/Client/.kiota/workspace.json new file mode 100644 index 0000000..3ce81de --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/.kiota/workspace.json @@ -0,0 +1,5 @@ +{ + "version": "1.0.0", + "clients": {}, + "plugins": {} +} \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs index bc98fc6..ea2afcd 100644 --- a/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs +++ b/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs @@ -2,8 +2,13 @@ #pragma warning disable CS0618 using CleanAspire.Api.Client.Account.ConfirmEmail; using CleanAspire.Api.Client.Account.DeleteOwnerAccount; +using CleanAspire.Api.Client.Account.Disable2fa; +using CleanAspire.Api.Client.Account.Enable2fa; using CleanAspire.Api.Client.Account.ForgotPassword; +using CleanAspire.Api.Client.Account.GenerateAuthenticator; +using CleanAspire.Api.Client.Account.GenerateRecoveryCodes; using CleanAspire.Api.Client.Account.Google; +using CleanAspire.Api.Client.Account.Login2fa; using CleanAspire.Api.Client.Account.Logout; using CleanAspire.Api.Client.Account.Profile; using CleanAspire.Api.Client.Account.Signup; @@ -32,16 +37,41 @@ public partial class AccountRequestBuilder : BaseRequestBuilder { get => new global::CleanAspire.Api.Client.Account.DeleteOwnerAccount.DeleteOwnerAccountRequestBuilder(PathParameters, RequestAdapter); } + /// The disable2fa property + public global::CleanAspire.Api.Client.Account.Disable2fa.Disable2faRequestBuilder Disable2fa + { + get => new global::CleanAspire.Api.Client.Account.Disable2fa.Disable2faRequestBuilder(PathParameters, RequestAdapter); + } + /// The enable2fa property + public global::CleanAspire.Api.Client.Account.Enable2fa.Enable2faRequestBuilder Enable2fa + { + get => new global::CleanAspire.Api.Client.Account.Enable2fa.Enable2faRequestBuilder(PathParameters, RequestAdapter); + } /// The forgotPassword property public global::CleanAspire.Api.Client.Account.ForgotPassword.ForgotPasswordRequestBuilder ForgotPassword { get => new global::CleanAspire.Api.Client.Account.ForgotPassword.ForgotPasswordRequestBuilder(PathParameters, RequestAdapter); } + /// The generateAuthenticator property + public global::CleanAspire.Api.Client.Account.GenerateAuthenticator.GenerateAuthenticatorRequestBuilder GenerateAuthenticator + { + get => new global::CleanAspire.Api.Client.Account.GenerateAuthenticator.GenerateAuthenticatorRequestBuilder(PathParameters, RequestAdapter); + } + /// The generateRecoveryCodes property + public global::CleanAspire.Api.Client.Account.GenerateRecoveryCodes.GenerateRecoveryCodesRequestBuilder GenerateRecoveryCodes + { + get => new global::CleanAspire.Api.Client.Account.GenerateRecoveryCodes.GenerateRecoveryCodesRequestBuilder(PathParameters, RequestAdapter); + } /// The google property public global::CleanAspire.Api.Client.Account.Google.GoogleRequestBuilder Google { get => new global::CleanAspire.Api.Client.Account.Google.GoogleRequestBuilder(PathParameters, RequestAdapter); } + /// The login2fa property + public global::CleanAspire.Api.Client.Account.Login2fa.Login2faRequestBuilder Login2fa + { + get => new global::CleanAspire.Api.Client.Account.Login2fa.Login2faRequestBuilder(PathParameters, RequestAdapter); + } /// The logout property public global::CleanAspire.Api.Client.Account.Logout.LogoutRequestBuilder Logout { diff --git a/src/CleanAspire.ClientApp/Client/Account/Disable2fa/Disable2faRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/Disable2fa/Disable2faRequestBuilder.cs new file mode 100644 index 0000000..228d0fe --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/Disable2fa/Disable2faRequestBuilder.cs @@ -0,0 +1,99 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Account.Disable2fa +{ + /// + /// Builds and executes requests for operations under \account\disable2fa + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Disable2faRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public Disable2faRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/disable2fa", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public Disable2faRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/disable2fa", rawUrl) + { + } + /// + /// This endpoint disables Two-Factor Authentication (TOTP) for a logged-in user. The user must already have 2FA enabled for this operation to be valid. + /// + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 404 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + var requestInfo = ToGetRequestInformation(requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// This endpoint disables Two-Factor Authentication (TOTP) for a logged-in user. The user must already have 2FA enabled for this operation to be valid. + /// + /// A + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + { +#endif + var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/problem+json"); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Account.Disable2fa.Disable2faRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.Disable2fa.Disable2faRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Disable2faRequestBuilderGetRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Account/Enable2fa/Enable2faRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/Enable2fa/Enable2faRequestBuilder.cs new file mode 100644 index 0000000..0680410 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/Enable2fa/Enable2faRequestBuilder.cs @@ -0,0 +1,104 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Account.Enable2fa +{ + /// + /// Builds and executes requests for operations under \account\enable2fa + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Enable2faRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public Enable2faRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/enable2fa", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public Enable2faRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/enable2fa", rawUrl) + { + } + /// + /// This endpoint enables Two-Factor Authentication (TOTP) for a logged-in user. The user must first scan the provided QR code using an authenticator app, and then verify the generated code to complete the process. + /// + /// A + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 404 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.Enable2faRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.Enable2faRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// This endpoint enables Two-Factor Authentication (TOTP) for a logged-in user. The user must first scan the provided QR code using an authenticator app, and then verify the generated code to complete the process. + /// + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.Enable2faRequest body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.Enable2faRequest body, Action> requestConfiguration = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/problem+json"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Account.Enable2fa.Enable2faRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.Enable2fa.Enable2faRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Enable2faRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Account/GenerateAuthenticator/GenerateAuthenticatorRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/GenerateAuthenticator/GenerateAuthenticatorRequestBuilder.cs new file mode 100644 index 0000000..5a5ea67 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/GenerateAuthenticator/GenerateAuthenticatorRequestBuilder.cs @@ -0,0 +1,117 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Account.GenerateAuthenticator +{ + /// + /// Builds and executes requests for operations under \account\generateAuthenticator + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class GenerateAuthenticatorRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public GenerateAuthenticatorRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/generateAuthenticator?appName={appName}", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public GenerateAuthenticatorRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/generateAuthenticator?appName={appName}", rawUrl) + { + } + /// + /// Generates a shared key and an Authenticator URI for a logged-in user. This endpoint is typically used to configure a TOTP authenticator app, such as Microsoft Authenticator or Google Authenticator. + /// + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 404 status code + /// When receiving a 500 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + var requestInfo = ToGetRequestInformation(requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "500", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.AuthenticatorResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Generates a shared key and an Authenticator URI for a logged-in user. This endpoint is typically used to configure a TOTP authenticator app, such as Microsoft Authenticator or Google Authenticator. + /// + /// A + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + { +#endif + var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json"); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Account.GenerateAuthenticator.GenerateAuthenticatorRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.GenerateAuthenticator.GenerateAuthenticatorRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Generates a shared key and an Authenticator URI for a logged-in user. This endpoint is typically used to configure a TOTP authenticator app, such as Microsoft Authenticator or Google Authenticator. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class GenerateAuthenticatorRequestBuilderGetQueryParameters + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + [QueryParameter("appName")] + public string? AppName { get; set; } +#nullable restore +#else + [QueryParameter("appName")] + public string AppName { get; set; } +#endif + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class GenerateAuthenticatorRequestBuilderGetRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Account/GenerateRecoveryCodes/GenerateRecoveryCodesRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/GenerateRecoveryCodes/GenerateRecoveryCodesRequestBuilder.cs new file mode 100644 index 0000000..9a228b4 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/GenerateRecoveryCodes/GenerateRecoveryCodesRequestBuilder.cs @@ -0,0 +1,99 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Account.GenerateRecoveryCodes +{ + /// + /// Builds and executes requests for operations under \account\generateRecoveryCodes + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class GenerateRecoveryCodesRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public GenerateRecoveryCodesRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/generateRecoveryCodes", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public GenerateRecoveryCodesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/generateRecoveryCodes", rawUrl) + { + } + /// + /// Generates new recovery codes if two-factor authentication is enabled. Returns 404 if the user is not found or 400 if 2FA is not enabled. + /// + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 404 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + var requestInfo = ToGetRequestInformation(requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.RecoveryCodesResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Generates new recovery codes if two-factor authentication is enabled. Returns 404 if the user is not found or 400 if 2FA is not enabled. + /// + /// A + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + { +#endif + var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json"); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Account.GenerateRecoveryCodes.GenerateRecoveryCodesRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.GenerateRecoveryCodes.GenerateRecoveryCodesRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class GenerateRecoveryCodesRequestBuilderGetRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Account/Login2fa/Login2faRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/Login2fa/Login2faRequestBuilder.cs new file mode 100644 index 0000000..b3c7a85 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/Login2fa/Login2faRequestBuilder.cs @@ -0,0 +1,117 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Account.Login2fa +{ + /// + /// Builds and executes requests for operations under \account\login2fa + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Login2faRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public Login2faRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/login2fa{?useCookies*,useSessionCookies*}", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public Login2faRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/login2fa{?useCookies*,useSessionCookies*}", rawUrl) + { + } + /// + /// This endpoint allows users to log in with their email and password. If two-factor authentication is enabled, the user must provide a valid two-factor code or recovery code. Supports persistent cookies or bearer tokens. + /// + /// A + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 401 status code + /// When receiving a 404 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.LoginRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.LoginRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "401", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// This endpoint allows users to log in with their email and password. If two-factor authentication is enabled, the user must provide a valid two-factor code or recovery code. Supports persistent cookies or bearer tokens. + /// + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.LoginRequest body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.LoginRequest body, Action> requestConfiguration = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/problem+json"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Account.Login2fa.Login2faRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.Login2fa.Login2faRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// This endpoint allows users to log in with their email and password. If two-factor authentication is enabled, the user must provide a valid two-factor code or recovery code. Supports persistent cookies or bearer tokens. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Login2faRequestBuilderPostQueryParameters + { + [QueryParameter("useCookies")] + public bool? UseCookies { get; set; } + [QueryParameter("useSessionCookies")] + public bool? UseSessionCookies { get; set; } + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class Login2faRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/AuthenticatorResponse.cs b/src/CleanAspire.ClientApp/Client/Models/AuthenticatorResponse.cs new file mode 100644 index 0000000..58bf6b4 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/AuthenticatorResponse.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class AuthenticatorResponse : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The authenticatorUri property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? AuthenticatorUri { get; set; } +#nullable restore +#else + public string AuthenticatorUri { get; set; } +#endif + /// The sharedKey property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? SharedKey { get; set; } +#nullable restore +#else + public string SharedKey { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public AuthenticatorResponse() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.AuthenticatorResponse CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.AuthenticatorResponse(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "authenticatorUri", n => { AuthenticatorUri = n.GetStringValue(); } }, + { "sharedKey", n => { SharedKey = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("authenticatorUri", AuthenticatorUri); + writer.WriteStringValue("sharedKey", SharedKey); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/Enable2faRequest.cs b/src/CleanAspire.ClientApp/Client/Models/Enable2faRequest.cs new file mode 100644 index 0000000..83c87ad --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/Enable2faRequest.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class Enable2faRequest : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The appName property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? AppName { get; set; } +#nullable restore +#else + public string AppName { get; set; } +#endif + /// The verificationCode property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? VerificationCode { get; set; } +#nullable restore +#else + public string VerificationCode { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public Enable2faRequest() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.Enable2faRequest CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.Enable2faRequest(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "appName", n => { AppName = n.GetStringValue(); } }, + { "verificationCode", n => { VerificationCode = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("appName", AppName); + writer.WriteStringValue("verificationCode", VerificationCode); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs b/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs index 62c4f72..52e5fda 100644 --- a/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs +++ b/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs @@ -32,6 +32,8 @@ public partial class ProfileResponse : IAdditionalDataHolder, IParsable #endif /// The isEmailConfirmed property public bool? IsEmailConfirmed { get; set; } + /// The isTwoFactorEnabled property + public bool? IsTwoFactorEnabled { get; set; } /// The languageCode property #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable @@ -124,6 +126,7 @@ public virtual IDictionary> GetFieldDeserializers() { "avatarUrl", n => { AvatarUrl = n.GetStringValue(); } }, { "email", n => { Email = n.GetStringValue(); } }, { "isEmailConfirmed", n => { IsEmailConfirmed = n.GetBoolValue(); } }, + { "isTwoFactorEnabled", n => { IsTwoFactorEnabled = n.GetBoolValue(); } }, { "languageCode", n => { LanguageCode = n.GetStringValue(); } }, { "nickname", n => { Nickname = n.GetStringValue(); } }, { "provider", n => { Provider = n.GetStringValue(); } }, @@ -144,6 +147,7 @@ public virtual void Serialize(ISerializationWriter writer) writer.WriteStringValue("avatarUrl", AvatarUrl); writer.WriteStringValue("email", Email); writer.WriteBoolValue("isEmailConfirmed", IsEmailConfirmed); + writer.WriteBoolValue("isTwoFactorEnabled", IsTwoFactorEnabled); writer.WriteStringValue("languageCode", LanguageCode); writer.WriteStringValue("nickname", Nickname); writer.WriteStringValue("provider", Provider); diff --git a/src/CleanAspire.ClientApp/Client/Models/RecoveryCodesResponse.cs b/src/CleanAspire.ClientApp/Client/Models/RecoveryCodesResponse.cs new file mode 100644 index 0000000..f1fecd7 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/RecoveryCodesResponse.cs @@ -0,0 +1,65 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class RecoveryCodesResponse : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The codes property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public List? Codes { get; set; } +#nullable restore +#else + public List Codes { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public RecoveryCodesResponse() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.RecoveryCodesResponse CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.RecoveryCodesResponse(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "codes", n => { Codes = n.GetCollectionOfPrimitiveValues()?.AsList(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteCollectionOfPrimitiveValues("codes", Codes); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Products/Export/ExportRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Products/Export/ExportRequestBuilder.cs index 136e04a..05ffb23 100644 --- a/src/CleanAspire.ClientApp/Client/Products/Export/ExportRequestBuilder.cs +++ b/src/CleanAspire.ClientApp/Client/Products/Export/ExportRequestBuilder.cs @@ -39,6 +39,7 @@ public ExportRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : bas /// A /// Cancellation token to use when cancelling requests /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code /// When receiving a 500 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable @@ -52,6 +53,7 @@ public async Task GetAsync(Action> { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, { "500", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, }; return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); diff --git a/src/CleanAspire.ClientApp/Components/PasswordInput.razor b/src/CleanAspire.ClientApp/Components/PasswordInput.razor new file mode 100644 index 0000000..171df2d --- /dev/null +++ b/src/CleanAspire.ClientApp/Components/PasswordInput.razor @@ -0,0 +1,50 @@ +@using System.Linq.Expressions + + + +@code { + [Parameter] public InputType InputType { get; set; } = InputType.Password; + [Parameter] public string AdornmentIcon { get; set; } = Icons.Material.Filled.VisibilityOff; + [Parameter] public string? Label { get; set; } + [Parameter] public string? Placeholder { get; set; } + [Parameter] public string? RequiredError { get; set; } + [Parameter] public Expression>? Field { get; set; } + [Parameter] public string Value { get; set; } = string.Empty; + [Parameter] public EventCallback ValueChanged { get; set; } + + private bool isVisible; + + private async Task ToggleVisibility() + { + isVisible = !isVisible; + AdornmentIcon = isVisible ? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff; + InputType = isVisible ? InputType.Text : InputType.Password; + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(Value); + } + } + + private async Task OnValueChanged(string newValue) + { + Value = newValue; + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(newValue); + } + } +} + + diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor index f9c4b3d..84345e2 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor @@ -1,8 +1,9 @@ -
+@using Net.Codecrete.QrCodeGenerator +
@L["Two-factor authentication"] - @L["Because of your contributions on GitHub, two-factor authentication will be required for your account starting Sep 21, 2023. Thank you for helping keep the ecosystem safe! Learn more about our two-factor authentication initiative."] + @L["For enhanced security, two-factor authentication will be required for your account. This helps protect your account with an additional layer of security."] @L["Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to sign in. Learn more about two-factor authentication."] @@ -20,28 +21,197 @@ @L["Authenticator app"]
@L["Use an authentication app or browser extension to get two-factor authentication codes when prompted."] + @if (UserProfileStore.Profile?.IsTwoFactorEnabled??false) + { +
+ + @L["To disable, click here."] + @L["Disable"] +
+ }
- +
- - @L["SMS/Text message"] - + + @L["Recovery codes"]
- @L["Get one-time codes sent to your phone via SMS to complete authentication requests."] + + @L["Generate recovery codes to regain access if you lose your two-factor authentication device."] + + + @if (recoveryCodes != null && recoveryCodes.Any()) + { + + @L["Important: Recovery codes are shown only once and will not be stored on the server. Please save them securely."] + +
+ + @L["Recovery codes generated. Save them securely as they will not be shown again."] + +
+ @foreach (var recoveryCode in recoveryCodes) + { + @recoveryCode + } +
+
+ } +
+ + @L["Generate Codes"] + + @if (recoveryCodes != null && recoveryCodes.Any()) + { + + @L["Copy Codes to Clipboard"] + + } +
+
-
+ + + + @L["Configure Authenticator App"] + + + +
+ @L["To use an authenticator app go through the following steps:"] + + 1. Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. + + + 2. Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.

+ @if (qrCode != null) + { +
@((MarkupString)qrCode)
+ } +
+ + 3. Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. + + +
+
+ + @L["Verify"] + @L["Close"] + +
@code { + private DialogOptions dialogOptions = new() { MaxWidth = MaxWidth.Small, CloseOnEscapeKey = true, CloseButton = true }; + private bool _showConfigureAuthenticatorAppDialog; + private string? qrCode; + private string? verificationCode; + private string sharedKey = "123456"; + private string authenticatorUri = "otpauth://totp/CleanAspire?secret"; + private List? recoveryCodes; + public async Task Disable2fa() + { + try + { + await ApiClient.Account.Disable2fa.GetAsync(); + var profile = UserProfileStore.Profile; + profile.IsTwoFactorEnabled = false; + UserProfileStore.Set(profile); + Snackbar.Add(L["Two-factor authentication has been disabled successfully"], Severity.Success); + } + catch(ProblemDetails e) + { + Snackbar.Add(e.Detail, Severity.Error); + } + catch (ApiException e) + { + Snackbar.Add(L["Failed to disable two-factor authentication"], Severity.Error); + } + + } + private async Task AuthenticatorAppValueChanged(bool value) + { + if (Navigation.BaseUri == "https://cleanaspire.blazorserver.com/" && UserProfileStore.Profile.Username == "Administrator") + { + Snackbar.Add(L["Test accounts cannot enable two-factor authentication in the demo environment. Please use your own account for testing."], Severity.Info); + return; + } + if (value) + { + var response = await ApiClient.Account.GenerateAuthenticator.GetAsync(q=>q.QueryParameters.AppName= AppSettings.AppName); + if (response is not null) + { + _showConfigureAuthenticatorAppDialog = true; + authenticatorUri = response.AuthenticatorUri; + sharedKey = response.SharedKey; + var qr = QrCode.EncodeText(response.AuthenticatorUri, QrCode.Ecc.Medium); + qrCode = qr.ToSvgString(4); + } + } + } + public Task Close() + { + _showConfigureAuthenticatorAppDialog = false; + return Task.CompletedTask; + } + public async Task Verify() + { + try + { + await ApiClient.Account.Enable2fa.PostAsync(new Enable2faRequest() { AppName = AppSettings.AppName, VerificationCode = verificationCode }); + var profile = UserProfileStore.Profile; + profile.IsTwoFactorEnabled = true; + UserProfileStore.Set(profile); + _showConfigureAuthenticatorAppDialog = false; + Snackbar.Add(L["Two-factor authentication has been enabled successfully"], Severity.Success); + } + catch(ApiException e) + { + Snackbar.Add(L["Invalid verification code"], Severity.Error); + } + + } + private async Task GenerateRecoveryCodes() + { + try + { + var response = await ApiClient.Account.GenerateRecoveryCodes.GetAsync(); + recoveryCodes = response.Codes; + Snackbar.Add(L["Recovery codes have been generated successfully. Please save them in a safe place."], Severity.Success); + } + catch (ApiException e) + { + Snackbar.Add(L["Failed to generate recovery codes"], Severity.Error); + } + } + private async Task CopyToClipboard() + { + if (recoveryCodes != null && recoveryCodes.Any()) + { + var codes = string.Join(Environment.NewLine, recoveryCodes); + await JS.InvokeVoidAsync("navigator.clipboard.writeText", codes); + Snackbar.Add(@L["Recovery codes copied to clipboard!"], Severity.Success); + } + else + { + Snackbar.Add(@L["No recovery codes to copy."], Severity.Warning); + } + } } diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignIn.razor b/src/CleanAspire.ClientApp/Pages/Account/SignIn.razor index 861ba51..f108d3b 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignIn.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignIn.razor @@ -9,29 +9,42 @@ @L[AppSettings.AppName]
- -
- @L["Don't have an account?"] @L["Signup"] -
- + @if (!RequiresTwoFactor) + { +
+ @L["Don't have an account?"] @L["Signup"] +
+ }
- - - -
- - - @L["Remember me"] - - @L["Forget password?"] -
- + @if (RequiresTwoFactor) + { + Your login is protected with an authenticator app. Enter your authenticator code below. + + + } + else + { + + + +
+ + + @L["Remember me"] + + @L["Forget password?"] +
+ } @L["Sign In"] - @L["Login with Google"] + @if (!RequiresTwoFactor){ + @L["Login with Google"] + }
@@ -47,23 +60,48 @@ Password = "P@ssw0rd!" }; - bool isShow; - InputType PasswordInput = InputType.Password; - string PasswordInputIcon = Icons.Material.Filled.VisibilityOff; - + + private bool RequiresTwoFactor = false; private async Task OnValidSubmit(EditContext context) { try { - await SignInManagement.LoginAsync(new Api.Client.Models.LoginRequest() { Email = model.Username, Password = model.Password }, model.RememberMe); + await SignInManagement.LoginAsync(new Api.Client.Models.LoginRequest() { Email = model.Username, Password = model.Password, TwoFactorCode = model.TwoFactorCode }, model.RememberMe); StateHasChanged(); } - catch (Exception e) + catch (ProblemDetails e) { - Logger.LogError(e, e.Message); - Snackbar.Add(L["Authentication failed.Please check your email and password and try again."], Severity.Error); + if (e.Detail == "RequiresTwoFactor") + { + RequiresTwoFactor = true; + } + else if (e.Detail == "NotAllowed") + { + Snackbar.Add(L["Your account is not activated. Please check your email for the activation link or contact support."], Severity.Error); + } + else if (RequiresTwoFactor && e.Detail == "Failed") + { + Snackbar.Add(L["Invalid verification code."], Severity.Error); + } + else + { + Snackbar.Add(L["Authentication failed.Please check your email and password and try again."], Severity.Error); + } + } + catch (ApiException e) + { + // Log and re-throw API exception + if(e.ResponseStatusCode== 404) + { + Snackbar.Add(L["User not found. Please check the email address and try again."], Severity.Error); + } + else + { + Snackbar.Add(L["Authentication failed.Please check your email and password and try again."], Severity.Error); + } } + } private async Task LoginWithGoogle() { @@ -74,7 +112,7 @@ { Navigation.NavigateTo(result); } - + } catch (Exception e) { @@ -83,21 +121,7 @@ } } - void ButtonPasswordClick() - { - if (isShow) - { - isShow = false; - PasswordInputIcon = Icons.Material.Filled.VisibilityOff; - PasswordInput = InputType.Password; - } - else - { - isShow = true; - PasswordInputIcon = Icons.Material.Filled.Visibility; - PasswordInput = InputType.Text; - } - } + public class SignInModel @@ -112,5 +136,7 @@ [Display(Name = "Remember Me")] public bool RememberMe { get; set; } = true; + [MaxLength(6)] + public string TwoFactorCode { get; set; } = string.Empty; } } diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor index 9e7c705..4c68297 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor @@ -26,8 +26,8 @@ - - + +
@@ -74,6 +74,7 @@ waiting = false; }); } + public class SignupModel { [Required] diff --git a/src/CleanAspire.ClientApp/Pages/Products/Index.razor b/src/CleanAspire.ClientApp/Pages/Products/Index.razor index fe6391f..8a046ef 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Index.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Index.razor @@ -1,6 +1,7 @@ @page "/products/index" @using CleanAspire.ClientApp.Pages.Products.Components @using CleanAspire.ClientApp.Services.Proxies +@using System.Globalization @inject ProductServiceProxy ProductServiceProxy @Title @@ -69,7 +70,7 @@ RowStyleFunc="_rowStyleFunc" - + @($"{context.Item.Price?.ToString("#,#")} {context.Item.Currency}") @@ -88,7 +89,12 @@ RowStyleFunc="_rowStyleFunc" private int _defaultPageSize = 10; private string _keywords = string.Empty; private bool _loading = false; - + AggregateDefinition _priceAggregation = new AggregateDefinition + { + Type = AggregateType.Sum, + NumberFormat = "#,#", + DisplayFormat = "Total amount is {value}" + }; private async Task> ServerReload(GridState state) { try diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs index 0c75905..e42cc9f 100644 --- a/src/CleanAspire.ClientApp/Program.cs +++ b/src/CleanAspire.ClientApp/Program.cs @@ -1,20 +1,5 @@ -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using CleanAspire.ClientApp; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Kiota.Abstractions.Authentication; -using Microsoft.Kiota.Http.HttpClientLibrary; -using CleanAspire.ClientApp.Services.Identity; -using CleanAspire.ClientApp.Configurations; -using CleanAspire.Api.Client; -using Microsoft.Kiota.Abstractions; -using Microsoft.Kiota.Serialization.Json; -using Microsoft.Kiota.Serialization.Text; -using Microsoft.Kiota.Serialization.Form; -using Microsoft.Kiota.Serialization.Multipart; -using CleanAspire.ClientApp.Services; -using CleanAspire.ClientApp.Services.JsInterop; -using CleanAspire.ClientApp.Services.Proxies; var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/src/CleanAspire.ClientApp/Services/Identity/AccessTokenProvider.cs b/src/CleanAspire.ClientApp/Services/Identity/AccessTokenProvider.cs deleted file mode 100644 index 97d98a3..0000000 --- a/src/CleanAspire.ClientApp/Services/Identity/AccessTokenProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using CleanAspire.ClientApp.Configurations; -using Microsoft.Kiota.Abstractions.Authentication; - -namespace CleanAspire.ClientApp.Services.Identity; - -public class AccessTokenProvider : IAccessTokenProvider -{ - - private readonly ClientAppSettings _clientAppSettings; - - public AccessTokenProvider (ClientAppSettings clientAppSettings) - { - _clientAppSettings = clientAppSettings; - } - - public AllowedHostsValidator AllowedHostsValidator => new AllowedHostsValidator(new[] { _clientAppSettings.ServiceBaseUrl}); - - public async Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = null, CancellationToken cancellationToken = default) - { - return string.Empty; - } -} - diff --git a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs index 123303e..2185d61 100644 --- a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs +++ b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs @@ -85,7 +85,7 @@ public async Task LoginAsync(LoginRequest request, bool remember = true, Cancell if (isOnline) { // Online login - var response = await apiClient.Login.PostAsync(request, options => + var response = await apiClient.Account.Login2fa.PostAsync(request, options => { options.QueryParameters.UseCookies = remember; options.QueryParameters.UseSessionCookies = !remember; @@ -108,12 +108,16 @@ public async Task LoginAsync(LoginRequest request, bool remember = true, Cancell // Refresh authentication state NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } - catch (ApiException ex) + catch (ProblemDetails) + { + throw; + } + catch (ApiException) { // Log and re-throw API exception throw; } - catch (Exception ex) + catch (Exception) { // Log and re-throw general exception throw; diff --git a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs index 25b9ded..7181065 100644 --- a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs @@ -7,7 +7,6 @@ using CleanAspire.ClientApp.Services.JsInterop; using Microsoft.AspNetCore.Components; using Microsoft.Kiota.Abstractions; -using MudBlazor.Charts; using OneOf; diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json index ec5bb2c..c2f30db 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.56", + "Version": "v0.0.57", "ServiceBaseUrl": "https://localhost:7341" } } diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json index a60ecc4..84e672f 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.56", + "Version": "v0.0.57", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } } diff --git a/src/CleanAspire.WebApp/appsettings.Development.json b/src/CleanAspire.WebApp/appsettings.Development.json index ec5bb2c..c2f30db 100644 --- a/src/CleanAspire.WebApp/appsettings.Development.json +++ b/src/CleanAspire.WebApp/appsettings.Development.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.56", + "Version": "v0.0.57", "ServiceBaseUrl": "https://localhost:7341" } } diff --git a/src/CleanAspire.WebApp/appsettings.json b/src/CleanAspire.WebApp/appsettings.json index 4677435..cce9a2f 100644 --- a/src/CleanAspire.WebApp/appsettings.json +++ b/src/CleanAspire.WebApp/appsettings.json @@ -8,7 +8,7 @@ "AllowedHosts": "*", "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.56", + "Version": "v0.0.57", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } }