1
0

Add login and register feature.

This commit is contained in:
Ca2didi 2025-03-23 06:33:26 +08:00
parent 9fcb843b9d
commit 6e156ee3a4
33 changed files with 647 additions and 144 deletions

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="fcb3a985-405e-4a4d-9f94-39584f3e89bb">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<configured-by-url>true</configured-by-url>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

8
.idea/Flawless-Version-Control.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="DBE_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Flawless-Version-Control.iml" filepath="$PROJECT_DIR$/.idea/Flawless-Version-Control.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

53
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="55aac044-7957-4f36-8d31-8e20fa18df86" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/Flawless.Abstract/HashUtility.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Communication/Dto/Auth/LoginDto.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Communication/Dto/Auth/RegisterDto.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Communication/Dto/Auth/TokenDto.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Server/Models/AppRole.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Server/Models/AppUser.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Server/Services/AppDbContext.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Server/Services/RefreshTokenContext.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Flawless.Server/Services/TokenService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless-Version-Control.sln.DotSettings.user" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless-Version-Control.sln.DotSettings.user" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Communication/Authentication/AuthenticationStatus.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Communication/Authentication/LoginRequest.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Communication/Authentication/RegisterRequest.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Communication/Authentication/RegisterResult.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Core/Flawless.Core.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Core/Flawless.Core.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Controllers/AuthenticationController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/Controllers/AuthenticationController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Flawless.Server.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/Flawless.Server.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Models/GlobalContext.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Models/RepositoryContext.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/Services/RepositoryContext.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Models/RepositoryContextFactory.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/Services/RepositoryContextFactory.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/Settings.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/appsettings.Development.json" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/appsettings.Development.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Flawless.Server/appsettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/Flawless.Server/appsettings.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 8
}]]></component>
<component name="ProjectId" id="2ufyen5fLDptXybcjFpGGF1UyHd" />
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenDatabaseViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master",
"ignore.virus.scanning.warn.message": "true",
"last_opened_file_path": "C:/Users/Cardi/Repos/Flawless-Version-Control"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
</project>

View File

@ -1,12 +1,25 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AActionMethodExecutor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdffdaf205cf54e098aa7d66ba76b38621de920_003F19_003Fe6aa02ee_003FActionMethodExecutor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentOutOfRangeException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F82_003F5d81019e_003FArgumentOutOfRangeException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssert_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea501b1a950043b99f3df638f1824d6143a18_003Fb8_003Fb16d6a68_003FAssert_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa5_003Ff3a8130e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticatorTokenProvider_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003Feb_003Fa2d5eee1_003FAuthenticatorTokenProvider_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimsPrincipal_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003F20_003Fabeaf9ae_003FClaimsPrincipal_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimTypes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003Fbd_003F4cde67a5_003FClaimTypes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fbc_003F2b4c89d0_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003Fcf_003F6a374370_003FIdentityDbContext_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUserToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Facfead36ed0138084f13ee724545d2dcb853f354ec658443b72fc26eff58781_003FIdentityUserToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc405819100144b0483c14b61d32c5aa215930_003F90_003F4d8e1a86_003FIdentityUser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d382df578ec93391918cfaa4ce7f4b8f35c9aed1241d6556dc9be26df13c_003FIdentityUser_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd991417b721d4ddab50a2b715d0ad696b138_003Fa2_003Fdb3874bc_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fce37be1a06b16c6faa02038d2cc477dd3bca5b217ceeb41c5f2ad45c1bf9_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASignInManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F40411c547364428dafc988a7615774e28b910_003F6d_003F1a409232_003FSignInManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATestMethodInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5ef53d675c5d34a6b85963919015dc0c1b06e5ea9834aac59ae6911f4c6f38_003FTestMethodInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003F9c_003F183f8355_003FUserManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValueTuple_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa7_003F76eb4679_003FValueTuple_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit&lt;/TestId&gt;

View File

@ -0,0 +1,17 @@
using System.Security.Cryptography;
using System.Text;
namespace Flawless.Abstraction;
public static class HashUtility
{
public static UInt128 StringToMd5Uint128(string input)
{
ReadOnlySpan<byte> source = Encoding.UTF8.GetBytes(input);
Span<byte> destination = stackalloc byte[16];
if (MD5.HashData(source, destination) != 16) throw new InvalidOperationException("MD5 hash length is invalid.");
return BitConverter.ToUInt128(destination);
}
}

View File

@ -1,7 +0,0 @@
namespace Flawless.Communication.Authentication;
public record AuthenticationStatus
{
public required bool OpenRegister { get; set; }
public required bool OpenLogin { get; set; }
}

View File

@ -1,10 +0,0 @@
namespace Flawless.Communication.Authentication;
public record LoginRequest
{
public required string Identification { get; init; }
public required string Password { get; init; }
public required bool DontExpireHalfMonth { get; set; }
}

View File

@ -1,12 +0,0 @@
namespace Flawless.Communication.Authentication;
public record RegisterRequest
{
public required string Email { get; set; }
public required string Username { get; set; }
public required string Password { get; set; }
public required bool DontExpireHalfMonth { get; set; }
}

View File

@ -1,10 +0,0 @@
namespace Flawless.Communication.Authentication;
public enum RegisterResultStatus
{
Success,
Forbidden,
Registered
}
public record RegisterResult(RegisterResultStatus Status);

View File

@ -0,0 +1,8 @@
namespace Flawless.Communication.Request.Auth;
public record LoginRequest
{
public required string Username { get; set; }
public required string Password { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace Flawless.Communication.Request.Auth;
public record RegisterRequest
{
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Flawless.Communication.Response;
public record FailedResponse(object message, string? failure = "Unknown")
{
public string? Failure { get; set; } = failure;
public object? Message { get; set; } = message;
}

View File

@ -0,0 +1,8 @@
namespace Flawless.Communication.Response;
public record UnintendedExceptionResponse(Exception e)
{
public string ExceptionType { get; } = e.GetType().Name;
public string ExceptionMessage { get; } = e.Message;
}

View File

@ -0,0 +1,6 @@
namespace Flawless.Communication.Shared;
public record TokenInfo
{
public required string Token { get; set; }
}

View File

@ -11,8 +11,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.3" />
<PackageReference Include="Nerdbank.Streams" Version="2.11.86" />
<PackageReference Include="SQLite" Version="3.13.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
<PackageReference Include="System.IO.Hashing" Version="9.0.3" />
</ItemGroup>

View File

@ -1,34 +1,137 @@
using Flawless.Communication.Authentication;
using System.Security.Claims;
using Flawless.Communication.Request.Auth;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
using Flawless.Server.Models;
using Flawless.Server.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Controllers;
[ApiController, Route("api/auth")]
public class AuthenticationController(GlobalContext dbContext, ILogger<AuthenticationController> logger)
public class AuthenticationController(
UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager,
TokenGenerationService tokenService,
AppDbContext dbContext,
IConfiguration config,
ILogger<AuthenticationController> logger)
: ControllerBase
{
[HttpGet("status")]
public ActionResult<AuthenticationStatus> GetStatus()
{
logger.LogInformation("Authentication status has sent to {0}", HttpContext.Connection.RemoteIpAddress);
return new AuthenticationStatus()
{
OpenRegister = true,
OpenLogin = true,
};
}
[HttpPost("register")]
public async Task<ActionResult<RegisterResult>> RegisterAsync(RegisterRequest request)
public async Task<IActionResult> PublicRegisterAsync(RegisterRequest request)
{
return BadRequest();
if (!config.GetValue("User:PublicRegister", true))
return BadRequest(new FailedResponse("Not opened for public register."));
var user = new AppUser
{
UserName = request.Username,
Email = request.Email,
EmailConfirmed = true,
CreatedOn = DateTime.UtcNow,
SecurityStamp = Guid.NewGuid().ToString()
};
var result = await userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
{
logger.LogInformation("User '{0}' created (PUBLIC REGISTER)", user.UserName);
return Ok();
}
logger.LogInformation("User '{0}' NOT created (PUBLIC REGISTER) : {1}", user.UserName, result.Errors);
return BadRequest(new FailedResponse(result.Errors));
}
[HttpPost("login")]
public async Task<ActionResult<string>> LoginAsync([FromBody] LoginRequest request)
public async Task<IActionResult> LoginAsync(LoginRequest r)
{
return "SuccessToken";
var user = await userManager.FindByNameAsync(r.Username);
if (user == null) return BadRequest(new FailedResponse("Invalid username or password."));
var result = await signInManager.CheckPasswordSignInAsync(user, r.Password, false);
if (result.Succeeded)
{
var refreshToken = tokenService.GenerateRefreshToken();
var claims = await GetClaimsAsync(user, refreshToken);
var jwtToken = tokenService.GenerateToken(claims);
var refKey = new AppUserRefreshKey
{
UserId = user.Id,
RefreshToken = refreshToken,
ExpireIn = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime),
};
dbContext.RefreshTokens.Add(refKey);
await dbContext.SaveChangesAsync();
await userManager.AddLoginAsync(user, new UserLoginInfo("login-interface", "", null));
return Ok(new TokenInfo { Token = jwtToken });
}
if (result.IsLockedOut)
{
return BadRequest(new FailedResponse("Account is locked out."));
}
return BadRequest(new FailedResponse("Invalid username or password."));
}
[HttpPost("refresh")]
public async Task<IActionResult> RefreshAsync(TokenInfo r)
{
var now = DateTime.UtcNow;
var set = dbContext.RefreshTokens;
var principal = tokenService.GetPrincipalFromExpiredToken(r.Token);
var user = await signInManager.ValidateSecurityStampAsync(principal);
if (user == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
try
{
// Remove timeout guys
await set.Where(k => k.ExpireIn < now).ExecuteDeleteAsync();
// Find valid expired refresh token
var refreshToken = principal.FindFirst(FlawlessClaimsType.RefreshToken)?.Value;
var tk = await set.FirstOrDefaultAsync(k => k.RefreshToken == refreshToken && k.UserId.ToString() == user.Id.ToString());
if (tk == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
// Renew keys
refreshToken = tokenService.GenerateRefreshToken();
var claims = await GetClaimsAsync(user, refreshToken);
var newJwtToken = tokenService.GenerateToken(claims);
// Reassign a new key.
set.Remove(tk);
set.Add(new AppUserRefreshKey
{
UserId = user.Id,
RefreshToken = refreshToken,
ExpireIn = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime),
});
return Ok(new TokenInfo { Token = newJwtToken });
}
finally
{
await dbContext.SaveChangesAsync();
}
}
private async ValueTask<IList<Claim>> GetClaimsAsync(AppUser user, string refreshToken)
{
var c = await userManager.GetClaimsAsync(user);
c.Add(new (FlawlessClaimsType.SecurityStamp, user.SecurityStamp ?? string.Empty));
c.Add(new (ClaimTypes.NameIdentifier, user.Id.ToString()));
c.Add(new (ClaimTypes.Name, user.UserName!));
c.Add(new (FlawlessClaimsType.RefreshToken, refreshToken));
return c;
}
}

View File

@ -8,9 +8,14 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.2.25163.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
@ -25,4 +30,15 @@
<ProjectReference Include="..\Flawless.Core\Flawless.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Migrations\AppDbContextModelSnapshot.cs" />
<Compile Remove="Migrations\20250322195500_Update.cs" />
<Compile Remove="Migrations\20250322195500_Update.Designer.cs" />
<Compile Remove="Migrations\20250322194407_InitialCreate.Designer.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace Flawless.Server;
public static class FlawlessClaimsType
{
public static readonly string SecurityStamp = "sest";
public static readonly string RefreshToken = "reftk";
}

View File

@ -0,0 +1,17 @@
using Flawless.Communication.Response;
namespace Flawless.Server.Middlewares;
public class ExceptionTransformMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
try { await next(context); }
catch (Exception e)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(
new FailedResponse(new UnintendedExceptionResponse(e), "UnintendedException"));
}
}
}

View File

@ -1,3 +1,5 @@
using Flawless.Communication.Response;
namespace Flawless.Server.Middlewares;
public class WebSocketHandoffMiddleware(RequestDelegate next)
@ -6,16 +8,14 @@ public class WebSocketHandoffMiddleware(RequestDelegate next)
{
// Is a WebSocket call and no ws presents.
if (context.Request.Path.StartsWithSegments("/ws") && !context.WebSockets.IsWebSocketRequest)
throw new InvalidOperationException("Connection is not a WebSocket!");
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(
new FailedResponse("Connection should be a WebSocket!", "NotWebSocketRequest"));
return;
}
await next(context);
}
}
public static class WebSocketHandoffMiddlewareExtensions
{
public static IApplicationBuilder UseWebSocketHandoffMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<WebSocketHandoffMiddleware>();
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity;
namespace Flawless.Server.Models;
public class AppUser : IdentityUser<Guid>
{
public DateTime CreatedOn { get; set; }
public void RenewSecurityStamp()
{
this.SecurityStamp = Guid.NewGuid().ToString();
}
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Flawless.Server.Models;
public class AppUserRefreshKey
{
[Key]
public required string RefreshToken { get; set; }
public Guid UserId { get; set; }
public DateTime ExpireIn { get; set; }
}

View File

@ -1,10 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Models;
public class GlobalContext : DbContext
{
public GlobalContext(DbContextOptions<GlobalContext> options) : base(options)
{
}
}

View File

@ -1,69 +1,173 @@
using Flawless.Server.Controllers;
using System.Security.Claims;
using System.Text;
using Flawless.Communication.Response;
using Flawless.Server.Middlewares;
using Flawless.Server.Models;
using Flawless.Server.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
namespace Flawless.Server;
// Api related
builder.Services.AddOpenApi();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSingleton<RepositoryContext>();
builder.Services.AddSwaggerGen(opt =>
public static class Program
{
opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods
opt.SupportNonNullableReferenceTypes();
});
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
ConfigAppService(builder);
ConfigAuthentication(builder);
ConfigDbContext(builder);
var app = builder.Build();
SetupWebApplication(app);
app.Run();
}
// Authentication related.
builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(opt =>
{
private static void ConfigAppService(WebApplicationBuilder builder)
{
// Api related
builder.Services.AddOpenApi();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(opt =>
{
opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods
opt.SupportNonNullableReferenceTypes();
});
}
});
private static void ConfigDbContext(WebApplicationBuilder builder)
{
// Data connection related.
builder.Services.AddDbContextFactory<RepositoryContext, RepositoryContextFactory>();
builder.Services.AddDbContext<AppDbContext>(opt =>
{
opt.UseNpgsql(builder.Configuration.GetConnectionString("CoreDb"));
});
// Data connection related.
builder.Services.AddDbContextFactory<RepositoryContext, RepositoryContextFactory>();
builder.Services.AddDbContext<GlobalContext>(opt =>
{
opt.UseInMemoryDatabase("Main");
});
builder.Services.AddIdentityCore<AppUser>(opt =>
{
opt.Password.RequireLowercase = false;
opt.Password.RequireUppercase = false;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireDigit = false;
opt.Password.RequiredLength = 6;
opt.User.RequireUniqueEmail = true;
opt.SignIn.RequireConfirmedAccount = false;
opt.SignIn.RequireConfirmedEmail = false;
opt.SignIn.RequireConfirmedPhoneNumber = false;
opt.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
opt.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
opt.ClaimsIdentity.SecurityStampClaimType = FlawlessClaimsType.SecurityStamp;
})
.AddSignInManager()
.AddEntityFrameworkStores<AppDbContext>();
}
var app = builder.Build();
private static void ConfigAuthentication(WebApplicationBuilder builder)
{
var config = builder.Configuration;
var secretKey = config["Jwt:SecretKey"] ?? throw new ApplicationException("No secret key found.");
var issuer = config["Jwt:Issuer"] ?? throw new ApplicationException("No issuer found.");
app.UseRouting();
// Config WebSocket support.
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(60),
KeepAliveTimeout = TimeSpan.FromSeconds(300),
});
app.UseWebSocketHandoffMiddleware();
// Authentication related.
builder.Services.AddSingleton<TokenGenerationService>();
builder.Services.AddAuthentication(opt =>
{
opt.DefaultScheme =
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
ClockSkew = TimeSpan.Zero,
RoleClaimType = ClaimTypes.Role,
NameClaimType = ClaimTypes.Name,
};
opt.Events = new JwtBearerEvents {
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) {
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnTokenValidated = async context =>
{
if (context.Principal != null)
{
var p = context.Principal!;
// Configure identity control
app.UseAuthentication();
app.UseAuthorization();
var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
// Configure actual controllers
app.MapControllers();
var stamp = p.FindFirst(FlawlessClaimsType.SecurityStamp)?.Value;
if (stamp == null) throw new SecurityTokenExpiredException("No valid SecurityStamp found.");
// Configure fallback endpoints
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
app.MapGet("/", () => Results.Redirect("/swagger/index.html"));
}
else
{
app.MapGet("/", () => "<p>Please use client app to open this server.</p>");
}
var db = context.HttpContext.RequestServices.GetRequiredService<UserManager<AppUser>>();
app.Run();
// Start validate user is existed and not expired.
var u = await db.FindByIdAsync(id!);
if (u == null) throw new SecurityTokenExpiredException("User is not existed in db.");
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
context.HttpContext.User = p;
context.Success();
}
}
};
});
// builder.Services.AddAuthorization(opt =>
// {
// opt.DefaultPolicy =
// })
}
private static void SetupWebApplication(WebApplication app)
{
app.UseMiddleware<ExceptionTransformMiddleware>();
app.UseRouting();
// Config WebSocket support.
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(60),
KeepAliveTimeout = TimeSpan.FromSeconds(300),
});
app.UseMiddleware<WebSocketHandoffMiddleware>();
// Configure identity control
app.UseAuthentication();
app.UseAuthorization();
// Configure actual controllers
app.MapControllers();
// Configure fallback endpoints
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
app.MapGet("/", () => Results.Redirect("/swagger/index.html"));
}
else
{
app.MapGet("/", () => "<p>Please use client app to open this server.</p>");
}
}
}

View File

@ -0,0 +1,12 @@
using Flawless.Server.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Services;
public class AppDbContext(DbContextOptions<AppDbContext> options)
: IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>(options)
{
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
}

View File

@ -1,15 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Models;
namespace Flawless.Server.Services;
public class RepositoryContext : DbContext
{
private readonly string RepositoryPath;
public RepositoryContext(IConfiguration config, DbContextOptions<GlobalContext> options) : base(options)
public RepositoryContext(IConfiguration config, DbContextOptions<RepositoryContext> options) : base(options)
{
var settings = config.GetSection("Settings").Get<Settings>();
RepositoryPath = settings?.DataStoragePath ?? "./Data";
RepositoryPath = Path.Combine(config["LocalStoragePath"] ?? "./Data", "Repository");
if (!Directory.Exists(RepositoryPath))
Directory.CreateDirectory(RepositoryPath);

View File

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
namespace Flawless.Server.Models;
namespace Flawless.Server.Services;
public class RepositoryContextFactory : DbContextFactory<RepositoryContext>
{

View File

@ -0,0 +1,93 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Flawless.Server.Services;
public class TokenGenerationService
{
private readonly SymmetricSecurityKey _key;
private readonly string _issuer;
private readonly double _expiresIn;
private readonly double _refreshTokenLifeTime;
public TokenGenerationService(IConfiguration c)
{
var rawKey = Encoding.UTF8.GetBytes(c["Jwt:SecretKey"] ?? throw new Exception("No Jwt:SecretKey"));
_key = new SymmetricSecurityKey(rawKey);
_issuer = c["Jwt:Issuer"] ?? throw new Exception("No Jwt:Issuer");
_expiresIn = double.Parse(c["Jwt:ExpiresIn"] ?? throw new Exception("No Jwt:ExpiresIn"));
_refreshTokenLifeTime = double.Parse(c["Jwt:RefreshTokenLifeTime"] ?? throw new Exception("No Jwt:RefreshTokenLifeTime"));
}
public double RefreshTokenLifeTime => _refreshTokenLifeTime;
public string GenerateToken(IEnumerable<Claim> claims)
{
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: _issuer,
claims: claims,
notBefore: now,
expires: now.AddMinutes(_expiresIn),
signingCredentials: new SigningCredentials(_key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidIssuer = _issuer,
IssuerSigningKey = _key,
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
public ClaimsPrincipal GetPrincipal(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _issuer,
IssuerSigningKey = _key,
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
public string GenerateRefreshToken()
{
Span<byte> randomNumber = stackalloc byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}

View File

@ -1,9 +0,0 @@
namespace Flawless.Server;
public sealed class Settings
{
// Local settings
public required string DataStoragePath { get; set; } = "./Data";
}

View File

@ -4,5 +4,19 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Password=postgres;Database=flawless"
},
"LocalStoragePath": "./data/development",
"User": {
"PublicRegister": false
},
"Jwt": {
"SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
"Issuer": "test",
"ExpiresIn": 30,
"RefreshTokenLifeTime": 7
}
}

View File

@ -5,8 +5,17 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Settings": {
"DataStoragePath": "/Users/hcm-b0485/Desktop/test"
"AllowedHosts": "*",
"ConnectionStrings": {
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Password=postgres;Database=flawless"
},
"LocalStoragePath": "./data/production",
"User": {
"PublicRegister": false
},
"Jwt": {
"SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
"Issuer": "test",
"ExpiresIn": 30
}
}