Add login and register feature.
This commit is contained in:
parent
9fcb843b9d
commit
6e156ee3a4
13
.idea/.idea.Flawless-Version-Control/.idea/dataSources.xml
generated
Normal file
13
.idea/.idea.Flawless-Version-Control/.idea/dataSources.xml
generated
Normal 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
8
.idea/Flawless-Version-Control.iml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
53
.idea/workspace.xml
generated
Normal 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>
|
||||
@ -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"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
|
||||
17
Flawless.Abstract/HashUtility.cs
Normal file
17
Flawless.Abstract/HashUtility.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace Flawless.Communication.Authentication;
|
||||
|
||||
public record AuthenticationStatus
|
||||
{
|
||||
public required bool OpenRegister { get; set; }
|
||||
public required bool OpenLogin { get; set; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
namespace Flawless.Communication.Authentication;
|
||||
|
||||
public enum RegisterResultStatus
|
||||
{
|
||||
Success,
|
||||
Forbidden,
|
||||
Registered
|
||||
}
|
||||
|
||||
public record RegisterResult(RegisterResultStatus Status);
|
||||
8
Flawless.Communication/Request/Auth/LoginRequest.cs
Normal file
8
Flawless.Communication/Request/Auth/LoginRequest.cs
Normal 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; }
|
||||
}
|
||||
10
Flawless.Communication/Request/Auth/RegisterRequest.cs
Normal file
10
Flawless.Communication/Request/Auth/RegisterRequest.cs
Normal 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; }
|
||||
}
|
||||
8
Flawless.Communication/Response/FailedResponse.cs
Normal file
8
Flawless.Communication/Response/FailedResponse.cs
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
6
Flawless.Communication/Shared/TokenInfo.cs
Normal file
6
Flawless.Communication/Shared/TokenInfo.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Flawless.Communication.Shared;
|
||||
|
||||
public record TokenInfo
|
||||
{
|
||||
public required string Token { get; set; }
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
8
Flawless.Server/FlawlessClaimsType.cs
Normal file
8
Flawless.Server/FlawlessClaimsType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Flawless.Server;
|
||||
|
||||
public static class FlawlessClaimsType
|
||||
{
|
||||
public static readonly string SecurityStamp = "sest";
|
||||
|
||||
public static readonly string RefreshToken = "reftk";
|
||||
}
|
||||
17
Flawless.Server/Middlewares/ExceptionTransformMiddleware.cs
Normal file
17
Flawless.Server/Middlewares/ExceptionTransformMiddleware.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
13
Flawless.Server/Models/AppUser.cs
Normal file
13
Flawless.Server/Models/AppUser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
13
Flawless.Server/Models/AppUserRefreshKey.cs
Normal file
13
Flawless.Server/Models/AppUserRefreshKey.cs
Normal 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; }
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
|
||||
public class GlobalContext : DbContext
|
||||
{
|
||||
public GlobalContext(DbContextOptions<GlobalContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Flawless.Server/Services/AppDbContext.cs
Normal file
12
Flawless.Server/Services/AppDbContext.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class RepositoryContextFactory : DbContextFactory<RepositoryContext>
|
||||
{
|
||||
93
Flawless.Server/Services/TokenGenerationService.cs
Normal file
93
Flawless.Server/Services/TokenGenerationService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
namespace Flawless.Server;
|
||||
|
||||
public sealed class Settings
|
||||
{
|
||||
// Local settings
|
||||
public required string DataStoragePath { get; set; } = "./Data";
|
||||
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user