diff --git a/src/CleanAspire.Api/CleanAspire.Api.csproj b/src/CleanAspire.Api/CleanAspire.Api.csproj index 2be8702..484e36c 100644 --- a/src/CleanAspire.Api/CleanAspire.Api.csproj +++ b/src/CleanAspire.Api/CleanAspire.Api.csproj @@ -18,6 +18,7 @@ + diff --git a/src/CleanAspire.Api/CleanAspire.Api.json b/src/CleanAspire.Api/CleanAspire.Api.json index eb74092..a67e4a1 100644 --- a/src/CleanAspire.Api/CleanAspire.Api.json +++ b/src/CleanAspire.Api/CleanAspire.Api.json @@ -414,8 +414,56 @@ "Authentication", "Identity Management" ], - "summary": "Retrieve the profile information", - "description": "This endpoint fetches the profile information of the currently authenticated user based on their claims. If the user is not found in the system, it returns a 404 Not Found status. The endpoint requires authorization and utilizes Identity Management for user retrieval and profile generation.", + "summary": "Retrieve the user's profile", + "description": "Fetches the profile information of the authenticated user. Returns 404 if the user is not found. Requires authorization.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/HttpValidationProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found" + } + }, + "security": [ + { + "Identity.Application": [ ] + } + ] + }, + "post": { + "tags": [ + "Authentication", + "Identity Management" + ], + "summary": "Update user profile information.", + "description": "Allows users to update their profile, including username, email, nickname, avatar, time zone, and language code.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileRequest" + } + } + }, + "required": true + }, "responses": { "200": { "description": "OK", @@ -593,6 +641,11 @@ } } }, + "IFormFile": { + "type": "string", + "format": "binary", + "nullable": true + }, "InfoRequest": { "type": "object", "properties": { @@ -652,6 +705,56 @@ "password": "P@ssw0rd!" } }, + "ProfileRequest": { + "required": [ + "username", + "email" + ], + "type": "object", + "properties": { + "nickname": { + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_]*$", + "type": "string", + "description": "User's preferred nickname.", + "nullable": true + }, + "username": { + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_]*$", + "type": "string", + "description": "Unique username for the user." + }, + "email": { + "maxLength": 80, + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "type": "string", + "description": "User's email address. Must be in a valid email format." + }, + "avatar": { + "$ref": "#/components/schemas/IFormFile" + }, + "timeZoneId": { + "maxLength": 50, + "type": "string", + "description": "User's time zone identifier, e.g., 'UTC', 'America/New_York'.", + "nullable": true + }, + "languageCode": { + "maxLength": 10, + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$", + "type": "string", + "description": "User's preferred language code, e.g., 'en-US'.", + "nullable": true + }, + "superiorId": { + "maxLength": 50, + "type": "string", + "description": "Tenant identifier for multi-tenant systems. Must be a GUID in version 7 format.", + "nullable": true + } + } + }, "ProfileResponse": { "required": [ "userId", @@ -729,7 +832,7 @@ } }, "example": { - "email": "Eddie.Torp73@hotmail.com", + "email": "Coleman.Lang@yahoo.com", "password": "P@ssw0rd!" } }, @@ -816,7 +919,7 @@ } }, "example": { - "Email": "Matteo_Breitenberg46@hotmail.com", + "Email": "Lavonne.Erdman@hotmail.com", "Password": "P@ssw0rd!", "Nickname": "exampleNickname", "Provider": "Local", diff --git a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs index f2a7d02..baf9bfe 100644 --- a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs +++ b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs @@ -15,10 +15,19 @@ using Microsoft.AspNetCore.WebUtilities; using System.Text.Encodings.Web; using System.Text; +using CleanAspire.Application.Common.Interfaces; +using CleanAspire.Infrastructure.Services; +using static System.Net.Mime.MediaTypeNames; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace CleanAspire.Api; public static class IdentityApiAdditionalEndpointsExtensions { + // Validate the email address using DataAnnotations like the UserValidator does when RequireUniqueEmail = true. + private static readonly EmailAddressAttribute _emailAddressAttribute = new(); + public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(this IEndpointRouteBuilder endpoints) where TUser : class, new() { @@ -44,11 +53,64 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(thi return TypedResults.NotFound(); } return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager)); - }).RequireAuthorization() - .WithSummary("Retrieve the profile information") - .WithDescription("This endpoint fetches the profile information of the currently authenticated user based on their claims. " + - "If the user is not found in the system, it returns a 404 Not Found status. " + - "The endpoint requires authorization and utilizes Identity Management for user retrieval and profile generation."); + }) + .WithSummary("Retrieve the user's profile") + .WithDescription("Fetches the profile information of the authenticated user. " + + "Returns 404 if the user is not found. Requires authorization."); + identityGroup.MapPost("/profile", async Task, ValidationProblem, NotFound>> + (ClaimsPrincipal claimsPrincipal, [FromBody] ProfileRequest request, HttpContext context, [FromServices] IServiceProvider sp) => + { + var userManager = sp.GetRequiredService>(); + if (await userManager.GetUserAsync(claimsPrincipal) is not { } user) + { + return TypedResults.NotFound(); + } + + if (!string.IsNullOrEmpty(request.Email) && !_emailAddressAttribute.IsValid(request.Email)) + { + return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(request.Email))); + } + if (user is not ApplicationUser appUser) + throw new InvalidCastException($"The provided user must be of type {nameof(ApplicationUser)}."); + + appUser.UserName = request.Username; + appUser.Nickname = request.Nickname; + appUser.TimeZoneId = request.TimeZoneId; + appUser.LanguageCode = request.LanguageCode; + appUser.SuperiorId = request.SuperiorId; + if (request.Avatar != null) + { + var avatarUrl = string.Empty; + var uploadService = sp.GetRequiredService(); + var filestream = request.Avatar.OpenReadStream(); + var imgstream = new MemoryStream(); + await filestream.CopyToAsync(imgstream); + imgstream.Position = 0; + using (var outStream = new MemoryStream()) + { + using (var image = SixLabors.ImageSharp.Image.Load(imgstream)) + { + image.Mutate(i => i.Resize(new ResizeOptions { Mode = ResizeMode.Crop, Size = new Size(128, 128) })); + image.Save(outStream, PngFormat.Instance); + avatarUrl = await uploadService.UploadAsync(new UploadRequest($"{appUser.Id}_{DateTime.UtcNow.Ticks}.png", UploadType.ProfilePicture, outStream.ToArray(), true)); + } + } + appUser.AvatarUrl = avatarUrl; + } + await userManager.UpdateAsync(user).ConfigureAwait(false); + if (!string.IsNullOrEmpty(request.Email)) + { + var email = await userManager.GetEmailAsync(user); + + if (email != request.Email) + { + await SendConfirmationEmailAsync(user, userManager, context, request.Email, isChange: true); + } + } + + return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager)); + }).WithSummary("Update user profile information.") + .WithDescription("Allows users to update their profile, including username, email, nickname, avatar, time zone, and language code."); @@ -213,6 +275,35 @@ private static ValidationProblem CreateValidationProblem(IdentityResult result) } } +public sealed class ProfileRequest +{ + [Description("User's preferred nickname.")] + [MaxLength(50, ErrorMessage = "Nickname cannot exceed 50 characters.")] + [RegularExpression("^[a-zA-Z0-9_]*$", ErrorMessage = "Nickname can only contain letters, numbers, and underscores.")] + public string? Nickname { get; init; } + + [Description("Unique username for the user.")] + [MaxLength(50, ErrorMessage = "Username cannot exceed 50 characters.")] + [RegularExpression("^[a-zA-Z0-9_]*$", ErrorMessage = "Username can only contain letters, numbers, and underscores.")] + public required string Username { get; init; } + [Required] + [Description("User's email address. Must be in a valid email format.")] + [MaxLength(80, ErrorMessage = "Email cannot exceed 80 characters.")] + [RegularExpression("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", ErrorMessage = "Invalid email format.")] + public required string Email { get; init; } + public IFormFile? Avatar { get; init; } + [Description("User's time zone identifier, e.g., 'UTC', 'America/New_York'.")] + [MaxLength(50, ErrorMessage = "TimeZoneId cannot exceed 50 characters.")] + public string? TimeZoneId { get; set; } + + [Description("User's preferred language code, e.g., 'en-US'.")] + [MaxLength(10, ErrorMessage = "LanguageCode cannot exceed 10 characters.")] + [RegularExpression("^[a-z]{2,3}(-[A-Z]{2})?$", ErrorMessage = "Invalid language code format.")] + public string? LanguageCode { get; set; } + [Description("Tenant identifier for multi-tenant systems. Must be a GUID in version 7 format.")] + [MaxLength(50, ErrorMessage = "Nickname cannot exceed 50 characters.")] + public string? SuperiorId { get; init; } +} public sealed class ProfileResponse { public string? Nickname { get; init; } diff --git a/src/CleanAspire.Application/Common/Interfaces/IUploadService.cs b/src/CleanAspire.Application/Common/Interfaces/IUploadService.cs new file mode 100644 index 0000000..9f30544 --- /dev/null +++ b/src/CleanAspire.Application/Common/Interfaces/IUploadService.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace CleanAspire.Application.Common.Interfaces; + +public interface IUploadService +{ + Task UploadAsync(UploadRequest request); + void Remove(string filename); +} +public record UploadRequest( + string FileName, + UploadType UploadType, + byte[] Data, + bool Overwrite = false, + string? Extension = null, + string? Folder = null +); +public enum UploadType : byte +{ + [Description(@"Products")] Product, + [Description(@"ProfilePictures")] ProfilePicture, + [Description(@"Documents")] Document +} diff --git a/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs new file mode 100644 index 0000000..b947c4b --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/AccountRequestBuilder.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Account.ConfirmEmail; +using CleanAspire.Api.Client.Account.Signup; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System; +namespace CleanAspire.Api.Client.Account +{ + /// + /// Builds and executes requests for operations under \account + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class AccountRequestBuilder : BaseRequestBuilder + { + /// The confirmEmail property + public global::CleanAspire.Api.Client.Account.ConfirmEmail.ConfirmEmailRequestBuilder ConfirmEmail + { + get => new global::CleanAspire.Api.Client.Account.ConfirmEmail.ConfirmEmailRequestBuilder(PathParameters, RequestAdapter); + } + /// The signup property + public global::CleanAspire.Api.Client.Account.Signup.SignupRequestBuilder Signup + { + get => new global::CleanAspire.Api.Client.Account.Signup.SignupRequestBuilder(PathParameters, RequestAdapter); + } + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public AccountRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account", 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 AccountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account", rawUrl) + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Account/ConfirmEmail/ConfirmEmailRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/ConfirmEmail/ConfirmEmailRequestBuilder.cs new file mode 100644 index 0000000..968de1c --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Account/ConfirmEmail/ConfirmEmailRequestBuilder.cs @@ -0,0 +1,117 @@ +// +#pragma warning disable CS0618 +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.ConfirmEmail +{ + /// + /// Builds and executes requests for operations under \account\confirmEmail + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class ConfirmEmailRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public ConfirmEmailRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/confirmEmail?code={code}&userId={userId}{&changedEmail*}", 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 ConfirmEmailRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/confirmEmail?code={code}&userId={userId}{&changedEmail*}", rawUrl) + { + } + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. +#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); + return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false); + } + /// 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); + 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.ConfirmEmail.ConfirmEmailRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Account.ConfirmEmail.ConfirmEmailRequestBuilder(rawUrl, RequestAdapter); + } + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class ConfirmEmailRequestBuilderGetQueryParameters + #pragma warning restore CS1591 + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + [QueryParameter("changedEmail")] + public string? ChangedEmail { get; set; } +#nullable restore +#else + [QueryParameter("changedEmail")] + public string ChangedEmail { get; set; } +#endif +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + [QueryParameter("code")] + public string? Code { get; set; } +#nullable restore +#else + [QueryParameter("code")] + public string Code { get; set; } +#endif +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + [QueryParameter("userId")] + public string? UserId { get; set; } +#nullable restore +#else + [QueryParameter("userId")] + public string UserId { 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 ConfirmEmailRequestBuilderGetRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Profile/ProfileRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Account/Signup/SignupRequestBuilder.cs similarity index 50% rename from src/CleanAspire.ClientApp/Client/Profile/ProfileRequestBuilder.cs rename to src/CleanAspire.ClientApp/Client/Account/Signup/SignupRequestBuilder.cs index 771fbc0..ab34c55 100644 --- a/src/CleanAspire.ClientApp/Client/Profile/ProfileRequestBuilder.cs +++ b/src/CleanAspire.ClientApp/Client/Account/Signup/SignupRequestBuilder.cs @@ -9,87 +9,86 @@ using System.Threading.Tasks; using System.Threading; using System; -namespace CleanAspire.Api.Client.Profile +namespace CleanAspire.Api.Client.Account.Signup { /// - /// Builds and executes requests for operations under \profile + /// Builds and executes requests for operations under \account\signup /// [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] - public partial class ProfileRequestBuilder : BaseRequestBuilder + public partial class SignupRequestBuilder : BaseRequestBuilder { /// - /// Instantiates a new and sets the default values. + /// Instantiates a new and sets the default values. /// /// Path parameters for the request /// The request adapter to use to execute the requests. - public ProfileRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/profile", pathParameters) + public SignupRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/signup", pathParameters) { } /// - /// Instantiates a new and sets the default values. + /// 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 ProfileRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/profile", rawUrl) + public SignupRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/account/signup", rawUrl) { } - /// - /// This endpoint fetches the profile information of the currently authenticated user based on their claims. If the user is not found in the system, it returns a 404 Not Found status. The endpoint requires authorization and utilizes Identity Management for user retrieval and profile generation. - /// - /// A + /// 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 #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task PostAsync(global::CleanAspire.Api.Client.Models.SignupRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) { #nullable restore #else - public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task PostAsync(global::CleanAspire.Api.Client.Models.SignupRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) { #endif - var requestInfo = ToGetRequestInformation(requestConfiguration); + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); var errorMapping = new Dictionary> { { "400", global::CleanAspire.Api.Client.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue }, }; - return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.ProfileResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); } - /// - /// This endpoint fetches the profile information of the currently authenticated user based on their claims. If the user is not found in the system, it returns a 404 Not Found status. The endpoint requires authorization and utilizes Identity Management for user retrieval and profile generation. - /// /// 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 ToGetRequestInformation(Action>? requestConfiguration = default) + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.SignupRequest body, Action>? requestConfiguration = default) { #nullable restore #else - public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.SignupRequest body, Action> requestConfiguration = default) { #endif - var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); requestInfo.Configure(requestConfiguration); - requestInfo.Headers.TryAdd("Accept", "application/json"); + 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 + /// A /// The raw URL to use for the request builder. - public global::CleanAspire.Api.Client.Profile.ProfileRequestBuilder WithUrl(string rawUrl) + public global::CleanAspire.Api.Client.Account.Signup.SignupRequestBuilder WithUrl(string rawUrl) { - return new global::CleanAspire.Api.Client.Profile.ProfileRequestBuilder(rawUrl, RequestAdapter); + return new global::CleanAspire.Api.Client.Account.Signup.SignupRequestBuilder(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 ProfileRequestBuilderGetRequestConfiguration : RequestConfiguration + public partial class SignupRequestBuilderPostRequestConfiguration : RequestConfiguration { } } diff --git a/src/CleanAspire.ClientApp/Client/ApiClient.cs b/src/CleanAspire.ClientApp/Client/ApiClient.cs index 3a246ea..71ccb13 100644 --- a/src/CleanAspire.ClientApp/Client/ApiClient.cs +++ b/src/CleanAspire.ClientApp/Client/ApiClient.cs @@ -1,11 +1,11 @@ // #pragma warning disable CS0618 +using CleanAspire.Api.Client.Account; using CleanAspire.Api.Client.ConfirmEmail; using CleanAspire.Api.Client.ForgotPassword; +using CleanAspire.Api.Client.Identity; using CleanAspire.Api.Client.Login; -using CleanAspire.Api.Client.Logout; using CleanAspire.Api.Client.Manage; -using CleanAspire.Api.Client.Profile; using CleanAspire.Api.Client.Refresh; using CleanAspire.Api.Client.Register; using CleanAspire.Api.Client.ResendConfirmationEmail; @@ -25,6 +25,11 @@ namespace CleanAspire.Api.Client [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] public partial class ApiClient : BaseRequestBuilder { + /// The account property + public global::CleanAspire.Api.Client.Account.AccountRequestBuilder Account + { + get => new global::CleanAspire.Api.Client.Account.AccountRequestBuilder(PathParameters, RequestAdapter); + } /// The confirmEmail property public global::CleanAspire.Api.Client.ConfirmEmail.ConfirmEmailRequestBuilder ConfirmEmail { @@ -35,26 +40,21 @@ public partial class ApiClient : BaseRequestBuilder { get => new global::CleanAspire.Api.Client.ForgotPassword.ForgotPasswordRequestBuilder(PathParameters, RequestAdapter); } + /// The identity property + public global::CleanAspire.Api.Client.Identity.IdentityRequestBuilder Identity + { + get => new global::CleanAspire.Api.Client.Identity.IdentityRequestBuilder(PathParameters, RequestAdapter); + } /// The login property public global::CleanAspire.Api.Client.Login.LoginRequestBuilder Login { get => new global::CleanAspire.Api.Client.Login.LoginRequestBuilder(PathParameters, RequestAdapter); } - /// The logout property - public global::CleanAspire.Api.Client.Logout.LogoutRequestBuilder Logout - { - get => new global::CleanAspire.Api.Client.Logout.LogoutRequestBuilder(PathParameters, RequestAdapter); - } /// The manage property public global::CleanAspire.Api.Client.Manage.ManageRequestBuilder Manage { get => new global::CleanAspire.Api.Client.Manage.ManageRequestBuilder(PathParameters, RequestAdapter); } - /// The profile property - public global::CleanAspire.Api.Client.Profile.ProfileRequestBuilder Profile - { - get => new global::CleanAspire.Api.Client.Profile.ProfileRequestBuilder(PathParameters, RequestAdapter); - } /// The refresh property public global::CleanAspire.Api.Client.Refresh.RefreshRequestBuilder Refresh { diff --git a/src/CleanAspire.ClientApp/Client/Identity/IdentityRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Identity/IdentityRequestBuilder.cs new file mode 100644 index 0000000..c549d73 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Identity/IdentityRequestBuilder.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Identity.Logout; +using CleanAspire.Api.Client.Identity.Profile; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System; +namespace CleanAspire.Api.Client.Identity +{ + /// + /// Builds and executes requests for operations under \identity + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class IdentityRequestBuilder : BaseRequestBuilder + { + /// The logout property + public global::CleanAspire.Api.Client.Identity.Logout.LogoutRequestBuilder Logout + { + get => new global::CleanAspire.Api.Client.Identity.Logout.LogoutRequestBuilder(PathParameters, RequestAdapter); + } + /// The profile property + public global::CleanAspire.Api.Client.Identity.Profile.ProfileRequestBuilder Profile + { + get => new global::CleanAspire.Api.Client.Identity.Profile.ProfileRequestBuilder(PathParameters, RequestAdapter); + } + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public IdentityRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity", 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 IdentityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity", rawUrl) + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Logout/LogoutRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Identity/Logout/LogoutRequestBuilder.cs similarity index 86% rename from src/CleanAspire.ClientApp/Client/Logout/LogoutRequestBuilder.cs rename to src/CleanAspire.ClientApp/Client/Identity/Logout/LogoutRequestBuilder.cs index 8a9fc33..ee10c78 100644 --- a/src/CleanAspire.ClientApp/Client/Logout/LogoutRequestBuilder.cs +++ b/src/CleanAspire.ClientApp/Client/Identity/Logout/LogoutRequestBuilder.cs @@ -8,28 +8,28 @@ using System.Threading.Tasks; using System.Threading; using System; -namespace CleanAspire.Api.Client.Logout +namespace CleanAspire.Api.Client.Identity.Logout { /// - /// Builds and executes requests for operations under \logout + /// Builds and executes requests for operations under \identity\logout /// [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] public partial class LogoutRequestBuilder : BaseRequestBuilder { /// - /// Instantiates a new and sets the default values. + /// Instantiates a new and sets the default values. /// /// Path parameters for the request /// The request adapter to use to execute the requests. - public LogoutRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/logout", pathParameters) + public LogoutRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity/logout", pathParameters) { } /// - /// Instantiates a new and sets the default values. + /// 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 LogoutRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/logout", rawUrl) + public LogoutRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity/logout", rawUrl) { } /// @@ -71,11 +71,11 @@ public RequestInformation ToPostRequestInformation(Action /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. /// - /// A + /// A /// The raw URL to use for the request builder. - public global::CleanAspire.Api.Client.Logout.LogoutRequestBuilder WithUrl(string rawUrl) + public global::CleanAspire.Api.Client.Identity.Logout.LogoutRequestBuilder WithUrl(string rawUrl) { - return new global::CleanAspire.Api.Client.Logout.LogoutRequestBuilder(rawUrl, RequestAdapter); + return new global::CleanAspire.Api.Client.Identity.Logout.LogoutRequestBuilder(rawUrl, RequestAdapter); } /// /// Configuration for the request such as headers, query parameters, and middleware options. diff --git a/src/CleanAspire.ClientApp/Client/Identity/Profile/ProfileRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Identity/Profile/ProfileRequestBuilder.cs new file mode 100644 index 0000000..08fb4d7 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Identity/Profile/ProfileRequestBuilder.cs @@ -0,0 +1,152 @@ +// +#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.Identity.Profile +{ + /// + /// Builds and executes requests for operations under \identity\profile + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class ProfileRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public ProfileRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity/profile", 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 ProfileRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/identity/profile", rawUrl) + { + } + /// + /// Fetches the profile information of the authenticated user. Returns 404 if the user is not found. Requires authorization. + /// + /// 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 +#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 }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.ProfileResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Allows users to update their profile, including username, email, nickname, avatar, time zone, and language code. + /// + /// 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 +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.ProfileRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.ProfileRequest 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.HttpValidationProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.ProfileResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Fetches the profile information of the authenticated user. Returns 404 if the user is not found. Requires authorization. + /// + /// 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; + } + /// + /// Allows users to update their profile, including username, email, nickname, avatar, time zone, and language code. + /// + /// 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.ProfileRequest body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.ProfileRequest 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/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.Identity.Profile.ProfileRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Identity.Profile.ProfileRequestBuilder(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 ProfileRequestBuilderGetRequestConfiguration : RequestConfiguration + { + } + /// + /// 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 ProfileRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/ProfileRequest.cs b/src/CleanAspire.ClientApp/Client/Models/ProfileRequest.cs new file mode 100644 index 0000000..a48252b --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/ProfileRequest.cs @@ -0,0 +1,125 @@ +// +#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 ProfileRequest : 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 avatar property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Avatar { get; set; } +#nullable restore +#else + public string Avatar { get; set; } +#endif + /// User's email address. Must be in a valid email format. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Email { get; set; } +#nullable restore +#else + public string Email { get; set; } +#endif + /// User's preferred language code, e.g., 'en-US'. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? LanguageCode { get; set; } +#nullable restore +#else + public string LanguageCode { get; set; } +#endif + /// User's preferred nickname. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Nickname { get; set; } +#nullable restore +#else + public string Nickname { get; set; } +#endif + /// Tenant identifier for multi-tenant systems. Must be a GUID in version 7 format. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? SuperiorId { get; set; } +#nullable restore +#else + public string SuperiorId { get; set; } +#endif + /// User's time zone identifier, e.g., 'UTC', 'America/New_York'. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? TimeZoneId { get; set; } +#nullable restore +#else + public string TimeZoneId { get; set; } +#endif + /// Unique username for the user. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Username { get; set; } +#nullable restore +#else + public string Username { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public ProfileRequest() + { + 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.ProfileRequest CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.ProfileRequest(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "avatar", n => { Avatar = n.GetStringValue(); } }, + { "email", n => { Email = n.GetStringValue(); } }, + { "languageCode", n => { LanguageCode = n.GetStringValue(); } }, + { "nickname", n => { Nickname = n.GetStringValue(); } }, + { "superiorId", n => { SuperiorId = n.GetStringValue(); } }, + { "timeZoneId", n => { TimeZoneId = n.GetStringValue(); } }, + { "username", n => { Username = 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("avatar", Avatar); + writer.WriteStringValue("email", Email); + writer.WriteStringValue("languageCode", LanguageCode); + writer.WriteStringValue("nickname", Nickname); + writer.WriteStringValue("superiorId", SuperiorId); + writer.WriteStringValue("timeZoneId", TimeZoneId); + writer.WriteStringValue("username", Username); + 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 a51f81d..62c4f72 100644 --- a/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs +++ b/src/CleanAspire.ClientApp/Client/Models/ProfileResponse.cs @@ -14,13 +14,13 @@ public partial class ProfileResponse : IAdditionalDataHolder, IParsable { /// 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 avatar property + /// The avatarUrl property #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public string? Avatar { get; set; } + public string? AvatarUrl { get; set; } #nullable restore #else - public string Avatar { get; set; } + public string AvatarUrl { get; set; } #endif /// The email property #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER @@ -121,7 +121,7 @@ public virtual IDictionary> GetFieldDeserializers() { return new Dictionary> { - { "avatar", n => { Avatar = n.GetStringValue(); } }, + { "avatarUrl", n => { AvatarUrl = n.GetStringValue(); } }, { "email", n => { Email = n.GetStringValue(); } }, { "isEmailConfirmed", n => { IsEmailConfirmed = n.GetBoolValue(); } }, { "languageCode", n => { LanguageCode = n.GetStringValue(); } }, @@ -141,7 +141,7 @@ public virtual IDictionary> GetFieldDeserializers() public virtual void Serialize(ISerializationWriter writer) { _ = writer ?? throw new ArgumentNullException(nameof(writer)); - writer.WriteStringValue("avatar", Avatar); + writer.WriteStringValue("avatarUrl", AvatarUrl); writer.WriteStringValue("email", Email); writer.WriteBoolValue("isEmailConfirmed", IsEmailConfirmed); writer.WriteStringValue("languageCode", LanguageCode); diff --git a/src/CleanAspire.ClientApp/Client/Models/SignupRequest.cs b/src/CleanAspire.ClientApp/Client/Models/SignupRequest.cs new file mode 100644 index 0000000..7a0a353 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/SignupRequest.cs @@ -0,0 +1,125 @@ +// +#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 SignupRequest : 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; } + /// User's email address. Must be in a valid email format. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Email { get; set; } +#nullable restore +#else + public string Email { get; set; } +#endif + /// User's preferred language code, e.g., 'en-US'. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? LanguageCode { get; set; } +#nullable restore +#else + public string LanguageCode { get; set; } +#endif + /// User's preferred nickname. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Nickname { get; set; } +#nullable restore +#else + public string Nickname { get; set; } +#endif + /// User's password. Must meet the security criteria. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Password { get; set; } +#nullable restore +#else + public string Password { get; set; } +#endif + /// Authentication provider, e.g., Local or Google. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Provider { get; set; } +#nullable restore +#else + public string Provider { get; set; } +#endif + /// Tenant identifier for multi-tenant systems. Must be a GUID in version 7 format. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? TenantId { get; set; } +#nullable restore +#else + public string TenantId { get; set; } +#endif + /// User's time zone identifier, e.g., 'UTC', 'America/New_York'. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? TimeZoneId { get; set; } +#nullable restore +#else + public string TimeZoneId { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public SignupRequest() + { + 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.SignupRequest CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.SignupRequest(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "email", n => { Email = n.GetStringValue(); } }, + { "languageCode", n => { LanguageCode = n.GetStringValue(); } }, + { "nickname", n => { Nickname = n.GetStringValue(); } }, + { "password", n => { Password = n.GetStringValue(); } }, + { "provider", n => { Provider = n.GetStringValue(); } }, + { "tenantId", n => { TenantId = n.GetStringValue(); } }, + { "timeZoneId", n => { TimeZoneId = 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("email", Email); + writer.WriteStringValue("languageCode", LanguageCode); + writer.WriteStringValue("nickname", Nickname); + writer.WriteStringValue("password", Password); + writer.WriteStringValue("provider", Provider); + writer.WriteStringValue("tenantId", TenantId); + writer.WriteStringValue("timeZoneId", TimeZoneId); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/LanguageAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/LanguageAutocomplete.cs new file mode 100644 index 0000000..3162c4c --- /dev/null +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/LanguageAutocomplete.cs @@ -0,0 +1,37 @@ +using CleanAspire.ClientApp.Services; +using MudBlazor; + +namespace CleanAspire.ClientApp.Components.Autocompletes; + +public class LanguageAutocomplete : MudAutocomplete +{ + public LanguageAutocomplete() + { + SearchFunc = SearchFunc_; + Dense = true; + ResetValueOnEmptyText = true; + ToStringFunc = x => + { + var language = Languages.FirstOrDefault(lang => lang.Code.Equals(x, StringComparison.OrdinalIgnoreCase)); + return language != null ? $"{language.DisplayName}" : x; + }; + } + + private List Languages { get; set; } = Localization.SupportedLanguages.ToList(); + + private Task> SearchFunc_(string value, CancellationToken cancellation = default) + { + // 如果输入为空,返回完整的语言列表;否则进行模糊搜索 + return string.IsNullOrEmpty(value) + ? Task.FromResult(Languages.Select(lang => lang.Code).AsEnumerable()) + : Task.FromResult(Languages + .Where(lang => Contains(lang, value)) + .Select(lang => lang.Code)); + } + + private static bool Contains(LanguageCode language, string value) + { + return language.Code.Contains(value, StringComparison.InvariantCultureIgnoreCase) || + language.DisplayName.Contains(value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/TimeZoneAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/TimeZoneAutocomplete.cs new file mode 100644 index 0000000..d2ef9d2 --- /dev/null +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/TimeZoneAutocomplete.cs @@ -0,0 +1,35 @@ +using MudBlazor; + +namespace CleanAspire.ClientApp.Components.Autocompletes; + +public class TimeZoneAutocomplete : MudAutocomplete +{ + public TimeZoneAutocomplete() + { + SearchFunc = SearchFunc_; + Dense = true; + ResetValueOnEmptyText = true; + ToStringFunc = x => + { + var timeZone = TimeZones.FirstOrDefault(tz => tz.Id.Equals(x)); + return timeZone != null ? timeZone.DisplayName : x; + }; + } + + private List TimeZones { get; set; } = TimeZoneInfo.GetSystemTimeZones().ToList(); + + private Task> SearchFunc_(string value, CancellationToken cancellation = default) + { + return string.IsNullOrEmpty(value) + ? Task.FromResult(TimeZones.Select(tz => tz.Id).AsEnumerable()) + : Task.FromResult(TimeZones + .Where(tz => Contains(tz, value)) + .Select(tz => tz.Id)); + } + + private static bool Contains(TimeZoneInfo timeZone, string value) + { + return timeZone.DisplayName.Contains(value, StringComparison.InvariantCultureIgnoreCase) || + timeZone.Id.Contains(value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/src/CleanAspire.ClientApp/Components/LoadingButton/MudLoadingButton.razor b/src/CleanAspire.ClientApp/Components/LoadingButton/MudLoadingButton.razor new file mode 100644 index 0000000..adb104a --- /dev/null +++ b/src/CleanAspire.ClientApp/Components/LoadingButton/MudLoadingButton.razor @@ -0,0 +1,107 @@ +@inherits MudBaseButton + + + @if (_loading) + { + if (LoadingAdornment == Adornment.Start) + { + + } + + if (LoadingContent != null) + { + @LoadingContent + } + else + { + @ChildContent + } + + if (LoadingAdornment == Adornment.End) + { + + } + } + else + { + @ChildContent + } + +@code{ + private bool _loading; + /// + /// If applied the text will be added to the component. + /// + [Parameter] + public string? Label { get; set; } + /// + /// The color of the icon. It supports the theme colors. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Inherit; + /// + /// Icon placed before the text if set. + /// + [Parameter] + public string? StartIcon { get; set; } + /// + /// The color of the component. It supports the theme colors. + /// + [Parameter] + public Color Color { get; set; } = Color.Default; + /// + /// Placement of the loading adornment. Default is start. + /// + [Parameter] + public Adornment LoadingAdornment { get; set; } = Adornment.Start; + + /// + /// The Size of the component. + /// + [Parameter] + public Size Size { get; set; } = Size.Medium; + /// + /// The variant to use. + /// + [Parameter] + public Variant Variant { get; set; } = Variant.Text; + [Parameter] + public bool Loading + { + get => _loading; + set + { + if (_loading == value) + return; + + _loading = value; + StateHasChanged(); + } + } + [Parameter] + public RenderFragment ChildContent { get; set; } // Adding ChildContent parameter + + [Parameter] + public RenderFragment LoadingContent { get; set; } + + [Parameter] + public Color LoadingCircularColor { get; set; } = Color.Primary; + + /// + /// Determines if the button should be disabled when loading. + /// + [Parameter] + public bool DisableWhenLoading { get; set; } = true; + + private bool IsDisabled => Disabled || (Loading && DisableWhenLoading); + + private async Task OnClickAsync() + { + if (IsDisabled) + return; + + _loading = true; + await base.OnClick.InvokeAsync(null); + _loading = false; + } +} diff --git a/src/CleanAspire.ClientApp/Layout/UserMenu.razor b/src/CleanAspire.ClientApp/Layout/UserMenu.razor index b9969c3..bae7d1f 100644 --- a/src/CleanAspire.ClientApp/Layout/UserMenu.razor +++ b/src/CleanAspire.ClientApp/Layout/UserMenu.razor @@ -6,14 +6,14 @@ - @if (string.IsNullOrEmpty(userModel?.Avatar)) + @if (string.IsNullOrEmpty(userModel?.AvatarUrl)) { @userModel?.Username?.FirstOrDefault() } else { - + } @@ -54,7 +54,6 @@ public AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!; private ProfileResponse? userModel => UserProfileStore.Profile; - private string? GetAvatarUrl() => userModel?.Avatar; private async Task OnSignOut() { await IdentityManagement.LogoutAsync(); diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor index 0260625..edd78df 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor @@ -16,10 +16,10 @@
@L["Profile picture"] @L["Add a profile picture to personalize your account"] - @if (userModel?.Avatar != null) + @if (userModel?.AvatarUrl != null) { - + } else @@ -40,12 +40,25 @@
- @L["Name"] + @L["Nickname"] @userModel?.Nickname
- + +
+ @L["Language"] + @Localization.SupportedLanguages.FirstOrDefault(x=>x.Code== userModel?.LanguageCode)?.DisplayName + +
+
+ +
+ @L["Time zone"] + @TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(x => x.Id == userModel?.TimeZoneId)?.DisplayName + +
+
} diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/PublicProfile.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/PublicProfile.razor index f67dee7..a82be51 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/PublicProfile.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/PublicProfile.razor @@ -1,4 +1,4 @@ - +
@@ -7,7 +7,7 @@ - @L["Save Profile"] + @L["Save Profile"]
@if (!string.IsNullOrEmpty(model.AvatarUrl)) @@ -23,10 +23,8 @@
- + @@ -45,13 +43,13 @@ { model = new ProfileModel - { - Id = userModel?.UserId ?? string.Empty, - Email = userModel?.Email?? string.Empty, - Name = userModel?.Nickname ?? string.Empty, - Username = userModel?.Username ?? string.Empty, - Avatar = userModel?.Avatar, - }; + { + Id = userModel?.UserId ?? string.Empty, + Email = userModel?.Email ?? string.Empty, + Name = userModel?.Nickname ?? string.Empty, + Username = userModel?.Username ?? string.Empty, + AvatarUrl = userModel?.AvatarUrl, + }; } private async Task UploadFiles(IBrowserFile file) diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor index e772a34..f93e696 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor @@ -1,6 +1,8 @@ @page "/account/signup" @using System.ComponentModel.DataAnnotations +@using CleanAspire.ClientApp.Components.Autocompletes + @L["Signup"]
@@ -15,25 +17,30 @@
+ + +
@L["I agree to the terms and privacy"]
- @L["Signup"] + @L["Signup"]
@code { + private bool waiting = false; private SignupModel model = new(); private async Task OnValidSubmit(EditContext context) { try { - var result = await ApiClient.Register.PostAsync(new RegisterRequest() { Email = model.Email, Password = model.Password }); + waiting = true; + var result = await ApiClient.Account.Signup.PostAsync(new SignupRequest() { Email = model.Email, Password = model.Password, LanguageCode = model.LanguageCode, Nickname = model.Nickname, Provider = model.Provider, TimeZoneId = model.TimeZoneId, TenantId = model.TenantId }); Navigation.NavigateTo("/account/signupsuccessful"); } catch (Exception e) @@ -41,6 +48,10 @@ Logger.LogError(e, e.Message); Snackbar.Add(L["An error occurred while creating your account. Please try again later."], Severity.Error); } + finally + { + waiting = false; + } } public class SignupModel { @@ -56,5 +67,21 @@ public string ConfirmPassword { get; set; } = string.Empty; [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the privacy policy.")] public bool CheckPrivacy { get; set; } + [RegularExpression("^[a-zA-Z0-9_]*$", ErrorMessage = "Nickname can only contain letters, numbers, and underscores.")] + [MaxLength(50, ErrorMessage = "Nickname cannot exceed 50 characters.")] + public string? Nickname { get; set; } + [MaxLength(50, ErrorMessage = "TimeZoneId cannot exceed 50 characters.")] + public string? TimeZoneId { get; set; } + + [MaxLength(10, ErrorMessage = "LanguageCode cannot exceed 10 characters.")] + [RegularExpression("^[a-z]{2,3}(-[A-Z]{2})?$", ErrorMessage = "Invalid language code format.")] + public string? LanguageCode { get; set; } + [MaxLength(50, ErrorMessage = "Nickname cannot exceed 50 characters.")] + public string? SuperiorId { get; init; } = Guid.CreateVersion7().ToString(); + [MaxLength(50, ErrorMessage = "Tenant id cannot exceed 50 characters.")] + public string? TenantId { get; init; } = Guid.CreateVersion7().ToString(); + + [MaxLength(20, ErrorMessage = "Provider cannot exceed 20 characters.")] + public string? Provider { get; set; } = "Local"; } } diff --git a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs index fc9ade7..da63c49 100644 --- a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs +++ b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs @@ -25,7 +25,7 @@ public override async Task GetAuthenticationStateAsync() try { // the user info endpoint is secured, so if the user isn't logged in this will fail - var profileResponse = await apiClient.Profile.GetAsync(); + var profileResponse = await apiClient.Identity.Profile.GetAsync(); Console.WriteLine(profileResponse); profileStore.Set(profileResponse); if (profileResponse != null) @@ -71,7 +71,7 @@ await apiClient.Login.PostAsync(request, options => public async Task LogoutAsync(CancellationToken cancellationToken = default) { - await apiClient.Logout.PostAsync(cancellationToken: cancellationToken); + await apiClient.Identity.Logout.PostAsync(cancellationToken: cancellationToken); // need to refresh auth state NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } diff --git a/src/CleanAspire.ClientApp/Services/Localization.cs b/src/CleanAspire.ClientApp/Services/Localization.cs new file mode 100644 index 0000000..4a59f79 --- /dev/null +++ b/src/CleanAspire.ClientApp/Services/Localization.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CleanAspire.ClientApp.Services; + +public static class Localization +{ + public const string ResourcesPath = "Resources"; + + public static readonly LanguageCode[] SupportedLanguages = + { + new() + { + Code = "en-US", + DisplayName = "English (United States)" + }, + new() + { + Code = "de-DE", + DisplayName = "Deutsch (Deutschland)" + }, + new() + { + Code = "ru-RU", + DisplayName = "русский (Россия)" + }, + new() + { + Code = "fr-FR", + DisplayName = "français (France)" + }, + new() + { + Code = "ja-JP", + DisplayName = "日本語 (日本)" + }, + new() + { + Code = "km-KH", + DisplayName = "ខ្មែរ (កម្ពុជា)" + }, + new() + { + Code = "ca-ES", + DisplayName = "català (Espanya)" + }, + new() + { + Code = "es-ES", + DisplayName = "español (España)" + }, + new() + { + Code = "zh-CN", + DisplayName = "中文(简体,中国)" + }, + new() + { + Code = "ar-iq", + DisplayName = "Arabic" + }, + new() + { + Code = "ko-kr", + DisplayName = "한국어(대한민국)" + } + }; +} + +public class LanguageCode +{ + public string DisplayName { get; set; } = "en-US"; + public string Code { get; set; } = "English"; +} diff --git a/src/CleanAspire.Infrastructure/DependencyInjection.cs b/src/CleanAspire.Infrastructure/DependencyInjection.cs index 2a78e59..7c80896 100644 --- a/src/CleanAspire.Infrastructure/DependencyInjection.cs +++ b/src/CleanAspire.Infrastructure/DependencyInjection.cs @@ -41,7 +41,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi IConfiguration configuration) { services - .AddDatabase(configuration); + .AddDatabase(configuration) + .AddScoped(); return services; diff --git a/src/CleanAspire.Infrastructure/Services/UploadService.cs b/src/CleanAspire.Infrastructure/Services/UploadService.cs new file mode 100644 index 0000000..59ce28c --- /dev/null +++ b/src/CleanAspire.Infrastructure/Services/UploadService.cs @@ -0,0 +1,118 @@ +// 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 System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Common.Interfaces; + +namespace CleanAspire.Infrastructure.Services; + +/// +/// Service for uploading files. +/// +public class UploadService : IUploadService +{ + private static readonly string NumberPattern = " ({0})"; + + /// + /// Uploads a file asynchronously. + /// + /// The upload request. + /// The path of the uploaded file. + public async Task UploadAsync(UploadRequest request) + { + if (request.Data == null || !request.Data.Any()) return string.Empty; + + var folder = request.UploadType.ToString().ToLower(); + var folderName = Path.Combine("files", folder); + if (!string.IsNullOrEmpty(request.Folder)) + { + folderName = Path.Combine(folderName, request.Folder); + } + var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName); + if (!Directory.Exists(pathToSave)) + { + Directory.CreateDirectory(pathToSave); + } + + var fileName = request.FileName.Trim('"'); + var fullPath = Path.Combine(pathToSave, fileName); + var dbPath = Path.Combine(folderName, fileName); + + if (!request.Overwrite && File.Exists(dbPath)) + { + dbPath = NextAvailableFilename(dbPath); + fullPath = NextAvailableFilename(fullPath); + } + + await using (var stream = new FileStream(fullPath, FileMode.Create)) + { + await stream.WriteAsync(request.Data, 0, request.Data.Length); + } + + return dbPath; + } + /// + /// remove file + /// + /// + public void Remove(string filename) + { + var removefile = Path.Combine(Directory.GetCurrentDirectory(), filename); + if (File.Exists(removefile)) + { + File.Delete(removefile); + } + } + /// + /// Gets the next available filename based on the given path. + /// + /// The path to check for availability. + /// The next available filename. + public static string NextAvailableFilename(string path) + { + if (!File.Exists(path)) + return path; + + if (Path.HasExtension(path)) + return GetNextFilename(path.Insert(path.LastIndexOf(Path.GetExtension(path)), NumberPattern)); + + return GetNextFilename(path + NumberPattern); + } + + /// + /// Gets the next available filename based on the given pattern. + /// + /// The pattern to generate the filename. + /// The next available filename. + private static string GetNextFilename(string pattern) + { + var tmp = string.Format(pattern, 1); + + if (!File.Exists(tmp)) + return tmp; + + int min = 1, max = 2; + + while (File.Exists(string.Format(pattern, max))) + { + min = max; + max *= 2; + } + + while (max != min + 1) + { + var pivot = (max + min) / 2; + if (File.Exists(string.Format(pattern, pivot))) + min = pivot; + else + max = pivot; + } + + return string.Format(pattern, max); + } +}