@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);
+ }
+}