2 Commits

Author SHA1 Message Date
leonhoppe
d9fec99954 Merge pull request #4 from leonhoppe/release/v1.1.0
Release/v1.1.0
2024-09-26 12:34:07 +02:00
leonhoppe
dffe653a44 Merge pull request #1 from leonhoppe/dev
Finished v1.0
2024-08-04 12:47:55 +02:00
219 changed files with 2718 additions and 10235 deletions

View File

@@ -1,39 +0,0 @@
image: mcr.microsoft.com/dotnet/sdk:8.0
stages:
- build
- test
- publish
before_script:
- echo "Setting up environment"
- 'dotnet --version'
build:
stage: build
script:
- dotnet restore
- dotnet build --configuration Release --no-restore
artifacts:
paths:
- "**/bin/Release"
expire_in: 10 minutes
test:
stage: test
script:
- dotnet test --verbosity normal
dependencies:
- build
publish:
stage: publish
script:
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
- dotnet pack -c Release -o . /p:Version=$VERSION
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json; done
only:
- tags
dependencies:
- build
- test

View File

@@ -5,7 +5,7 @@
<driver-ref>sqlite.xerial</driver-ref> <driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver> <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db</jdbc-url> <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url>
<jdbc-additional-properties> <jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" /> <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties> </jdbc-additional-properties>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTest", "test\RestApiTest\RestApiTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
EndProject EndProject
@@ -10,23 +10,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Web", "testing\HopFrame.Testing.Web\HopFrame.Testing.Web.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{64EDCBED-A84F-4936-8697-78DC43CB2427}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{EEA20D27-D471-44AF-A287-C0E068D93182}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1D98E5DE-CB8B-4C1C-A319-D49AC137441A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Database", "tests\HopFrame.Tests.Database\HopFrame.Tests.Database.csproj", "{1CAAC943-B8FE-48DD-9712-92699647DE18}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Security", "tests\HopFrame.Tests.Security\HopFrame.Tests.Security.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Api", "tests\HopFrame.Tests.Api\HopFrame.Tests.Api.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -58,38 +42,7 @@ Global
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.Build.0 = Release|Any CPU
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = Release|Any CPU
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.Build.0 = Release|Any CPU
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
{003120AE-F38B-4632-8497-BE4505189627} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
{7F82E1C6-4A42-4337-9E03-2EE6429D004F} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
{3BE585BC-13A5-4BE4-A806-E9EC2D825956} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
{8F983A37-63CF-48D5-988D-58B78EF8AECD} = {EEA20D27-D471-44AF-A287-C0E068D93182}
{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {EEA20D27-D471-44AF-A287-C0E068D93182}
{1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
{6747753A-6059-48F1-B779-D73765A373A6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
{25DE1510-47E5-46FF-89A4-B9F99542218E} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -1,122 +1,5 @@
<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"> <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_003AAsyncValueTaskMethodBuilderT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F1e8feaadf5c3fa14d36ea2a638c432a2e1a47b7837d8b83d88303c5d9c15cf_003FAsyncValueTaskMethodBuilderT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fca451c12d69fe026a0e7e9b1a0ddbf4cf6f6b8316cb2aec7984a7241813f648_003FAuthenticationHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMvcCoreServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe3d0c01ead7c1fbe35a3504e9fbf28f212ac59349851c852a28fa06d719e95_003FMvcCoreServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenIdConnectConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2820ca73717f4b1ab2b3b74fd61961bd1d5b0_003F7e_003F8be6b109_003FOpenIdConnectConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedirectResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2261f0cc7e883c3c89b8e6c7ea509ac7c52d0629e68690ed216e1cc0c8ac_003FRedirectResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASwaggerGenServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F95fc571df74c88edcdd2fb9f2e804132aa893358f3e0d8bca643299aed8dbc_003FSwaggerGenServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD; <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD; &lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD; &lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String> &lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=027ac703_002Df1f3_002D42aa_002D9c67_002D7cbaeecdbead/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::25DE1510-47E5-46FF-89A4-B9F99542218E::net8.0::HopFrame.Tests.Api.Controllers.OpenIdControllerTests.Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

View File

@@ -5,17 +5,13 @@ A simple backend management api for ASP.NET Core Web APIs
- [x] Database management - [x] Database management
- [x] User authentication - [x] User authentication
- [x] Permission management - [x] Permission management
- [x] Generated frontend administration boards - [x] Frontend dashboards
- [x] API token support
- [x] OpenID authentication integration
# Usage # Usage
There are two different versions of HopFrame, either the Web API version or the full Blazor web version. There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
## Ho to use the Web API version ## Ho to use the Web API version
> **Hint:** For more information about the HopFrame installation and usage go to the [docs](./docs).
1. Add the HopFrame.Api library to your project: 1. Add the HopFrame.Api library to your project:
``` ```

View File

@@ -0,0 +1,26 @@
@startuml ApiModels
namespace HopFrame.Security {
class UserLogin {
+Email: string
+Password: string
}
class UserRegister {
+Username: string
+Email: string
+Password: string
}
}
namespace HopFrame.Api {
class SingleValueResult<TValue> {
+Value: TValue
}
class UserPasswordValidation {
+Password: string
}
}
@enduml

View File

@@ -0,0 +1,37 @@
@startuml BaseModels
set namespaceSeparator none
namespace HopFrame.Database {
class User {
+Id: Guid
+Username: string
+Email: string
+CreatedAt: DateTime
+Permissions: IList<Permission>
}
class Permission {
+Id: long
+PermissionName: string
+Owner: Guid
+GrantedAt: DateTime
}
class PermissionGroup {
+Name: string
+IsDefaultGroup: bool
+Description: string
+CreatedAt: DateTime
+Permissions: IList<Permission>
}
interface IPermissionOwner {}
}
IPermissionOwner <|-- User
IPermissionOwner <|-- PermissionGroup
User .. Permission
PermissionGroup .. Permission
@enduml

View File

@@ -0,0 +1,38 @@
@startuml DatabaseModels
set namespaceSeparator none
namespace HopFrame.Database {
class UserEntry {
+Id: string
+Username: string
+Email: string
+Password: string
+CreatedAt: DateTime
}
class TokenEntry {
+Type: int
+Token: string
+UserId: string
+CreatedAt: DateTime
}
class PermissionEntry {
+RecordId: long
+PermissionText: string
+UserId: string
+GrantedAt: DateTime
}
class GroupEntry {
+Name: string
+Default: bool
+Description: string
+CreatedAt: DateTime
}
}
UserEntry *-- TokenEntry
UserEntry *-- PermissionEntry
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

13
docs/README.md Normal file
View File

@@ -0,0 +1,13 @@
# HopFrame documentation
These sides contain all documentation available for the HopFrame modules
## Content
| Topic | Description | Document |
|----------|------------------------------------------------|-----------------------|
| Models | All models used by the HopFrame | [link](./models.md) |
| Services | All services provided by the HopFrame | [link](./services.md) |
| Usage | How to properly implement the HopFrame modules | [link](./usage.md) |
## Dependencies
Both the HopFrame.Api and HopFrame.Web modules are dependent on the HopFrame.Database and HopFrame.Security modules.
So all models and services provided by these modules are available in the other modules as well.

View File

@@ -1,30 +0,0 @@
# HopFrame Authentication
With the provided HopFrame services, you can secure your endpoints so that only logged-in users or users with the right permissions can access the endpoint.
## Usage
You can secure your endpoints by adding the `Authorized` attribute.
```csharp
// Everyone can access this endpoint
[HttpGet("hello")]
public ActionResult<string> HelloWorld() {
return "Hello, World!";
}
```
```csharp
// Only logged-in users can access this endpoint
[HttpGet("hello"), Authorized]
public ActionResult<string> HelloWorld() {
return "Hello, World!";
}
```
```csharp
// Only logged-in users with the specified permissions can access this endpoint
[HttpGet("hello"), Authorized("test.permission", "test.permission.another")]
public ActionResult<string> HelloWorld() {
return "Hello, World!";
}
```

View File

@@ -1,120 +0,0 @@
# Auth Endpoints
## Used Models
- [UserLogin](../../models.md#userlogin)
- [UserRegister](../../models.md#userregister)
- [SingleValueResult](../../models.md#singlevalueresult)
- [UserPasswordValidation](../../models.md#userpasswordvalidation)
## API Endpoint: Login
**Endpoint:** `PUT /api/v1/auth/login`
**Description:** Authenticates a user and provides access and refresh tokens.
**Authorization Required:** No
**Parameters:**
- **UserLogin** (required): The login credentials of the user.
```json
{
"email": "string",
"password": "string"
}
```
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled.
- **404 Not Found:** The provided email address was not found.
- **403 Forbidden:** The provided password is not correct.
## API Endpoint: Register
**Endpoint:** `POST /api/v1/auth/register`
**Description:** Registers a new user and provides access and refresh tokens.
**Authorization Required:** No
**Parameters:**
- **UserRegister** (required): The registration details of the user.
```json
{
"username": "string",
"email": "string",
"password": "string"
}
```
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled or the password is too short.
- **409 Conflict:** Username or email is already registered.
## API Endpoint: Authenticate
**Endpoint:** `GET /api/v1/auth/authenticate`
**Description:** Authenticates the user using the refresh token and provides a new access token.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled or refresh token not provided.
- **404 Not Found:** The refresh token is not valid.
- **403 Forbidden:** The refresh token is expired.
- **409 Conflict:** The provided token is not a refresh token.
## API Endpoint: Logout
**Endpoint:** `DELETE /api/v1/auth/logout`
**Description:** Logs out the user and deletes the access and refresh tokens.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** User is logged out successfully.
## API Endpoint: Delete
**Endpoint:** `DELETE /api/v1/auth/delete`
**Description:** Deletes the user account.
**Authorization Required:** Yes
**Parameters:**
- **UserPasswordValidation** (required): The password validation for the user.
```json
{
"password": "string"
}
```
**Response:**
- **200 OK:** User account is deleted successfully.
- **403 Forbidden:** The provided password is not correct.

View File

@@ -1,237 +0,0 @@
# Group Endpoints
## Used Models
- [Group](../../models.md#permissiongroup)
## API Endpoint: GetGroups
**Endpoint:** `GET /api/v1/groups`
**Description:** Retrieves a list of all groups.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of groups.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetDefaultGroups
**Endpoint:** `GET /api/v1/groups/default`
**Description:** Retrieves a list of default groups.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of default groups.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetUserGroups
**Endpoint:** `GET /api/v1/groups/user/{userId}`
**Description:** Retrieves a list of groups for a specific user.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:**
- **userId:** `string` (path) - The ID of the user.
**Response:**
- **200 OK:** Returns a list of groups for the user.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **400 Bad Request:** Invalid user ID.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetGroup
**Endpoint:** `GET /api/v1/groups/{name}`
**Description:** Retrieves details of a specific group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:**
- **name:** `string` (path) - The name of the group.
**Response:**
- **200 OK:** Returns the details of the group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.
## API Endpoint: CreateGroup
**Endpoint:** `POST /api/v1/groups`
**Description:** Creates a new group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.create`
**Parameters:**
- **group:** `PermissionGroup` (body) - The group to be created.
**Response:**
- **200 OK:** Returns the created group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Provide a group.
- **400 Bad Request:** Group names must start with 'group.'.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **409 Conflict:** Group already exists.
## API Endpoint: UpdateGroup
**Endpoint:** `PUT /api/v1/groups`
**Description:** Updates an existing group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.update`
**Parameters:**
- **group:** `PermissionGroup` (body) - The group to be updated.
**Response:**
- **200 OK:** Returns the updated group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.
## API Endpoint: DeleteGroup
**Endpoint:** `DELETE /api/v1/groups/{name}`
**Description:** Deletes a specific group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.delete`
**Parameters:**
- **name:** `string` (path) - The name of the group to be deleted.
**Response:**
- **200 OK:** Group deleted successfully.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.

View File

@@ -1,82 +0,0 @@
# OpenID Endpoints
## Used Models
- [SingleValueResult](../../models.md#singlevalueresult)
## API Endpoint: RedirectToProvider
**Endpoint:** `GET /api/v1/openid/redirect`
**Description:** Redirects the user to the OpenID provider's authorization endpoint.
**Authorization Required:** No
**Parameters:**
- **redirectAfter** (query, optional): The URL to redirect to after authentication.
- **performRedirect** (query, optional): A flag to indicate if the user should be redirected (default is 1).
**Response:**
- **302 Found:** Redirects the user to the OpenID provider's authorization endpoint.
- **200 OK:** Returns the constructed authorization URI.
```json
{
"value": "string"
}
```
## API Endpoint: Callback
**Endpoint:** `GET /api/v1/openid/callback`
**Description:** Handles the callback from the OpenID provider and exchanges the authorization code for tokens.
**Authorization Required:** No
**Parameters:**
- **code** (query, required): The authorization code received from the OpenID provider.
- **state** (query, optional): The state parameter to handle the redirect after authentication.
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** Authorization code is missing.
- **403 Forbidden:** Authorization code is not valid.
## API Endpoint: Refresh
**Endpoint:** `GET /api/v1/openid/refresh`
**Description:** Refreshes the access token using the refresh token.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** Returns the refreshed access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** Refresh token not provided.
- **409 Conflict**: Refresh token not valid.
## API Endpoint: Logout
**Endpoint:** `DELETE /api/v1/openid/logout`
**Description:** Logs out the user by deleting the authentication cookies.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** User is logged out successfully.

View File

@@ -1,316 +0,0 @@
# User Endpoints
## Used Models
- [User](../../models.md#user)
- [UserCreator](../../models.md#usercreator)
- [Permission](../../models.md#permission)
## API Endpoint: GetUsers
**Endpoint:** `GET /api/v1/users`
**Description:** Retrieves a list of users.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of users.
```json
[
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetUser
**Endpoint:** `GET /api/v1/users/{userId}`
**Description:** Retrieves a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetUserByUsername
**Endpoint:** `GET /api/v1/users/username/{username}`
**Description:** Retrieves a user by their username.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **username:** `string` (Path) - The username of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetUserByEmail
**Endpoint:** `GET /api/v1/users/email/{email}`
**Description:** Retrieves a user by their email address.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **email:** `string` (Path) - The email address of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: CreateUser
**Endpoint:** `POST /api/v1/users`
**Description:** Creates a new user.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.create`
**Parameters:**
- **UserCreator:** (Body) - The user creation details.
```json
{
"Username": "string",
"Email": "string",
"Password": "string",
"Permissions": [
"permission1",
"permission2"
]
}
```
**Response:**
- **200 OK:** Returns the created user.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **409 Conflict:** The user already exists.
## API Endpoint: UpdateUser
**Endpoint:** `PUT /api/v1/users/{userId}`
**Description:** Updates an existing user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.update`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to update.
- **User:** (Body) - The user details to update.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
**Response:**
- **200 OK:** Returns the updated user.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
- **409 Conflict:** Cannot edit user with different user ID.
## API Endpoint: DeleteUser
**Endpoint:** `DELETE /api/v1/users/{userId}`
**Description:** Deletes a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.delete`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to delete.
**Response:**
- **200 OK:** User successfully deleted.
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: ChangePassword
**Endpoint:** `PUT /api/v1/users/{userId}/password`
**Description:** Updates the password for a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.update` (if the userId is not the id of the requesting user)
**Parameters:**
- **userId:** `string` (Path) - The ID of the user whose password is being changed.
- **UserPasswordChange:** (Body) - The password change details (note, if you change someone else's password the old password doesn't need to be correct).
```json
{
"oldPassword": "string",
"newPassword": "string"
}
```
**Response:**
- **200 OK:** Password successfully updated.
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
- **409 Conflict:** Old password is incorrect.

View File

@@ -1,16 +0,0 @@
# Ho to use the Web API version
This Installation adds all HopFrame [endpoints](./endpoints.md) and [repositories](../repositories.md) to the application.
1. Add the HopFrame.Api library to your project:
```
dotnet add package HopFrame.Api
```
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
```csharp
builder.Services.AddHopFrame<DatabaseContext>();
```

View File

@@ -1,33 +0,0 @@
# LogicResults
LogicResults provide another layer of abstraction above the ActionResults.
They help you sending the right `HttpStatusCode` with the right data.
## Usage
1. Create an endpoint that returns an `ActionResult`:
```csharp
[HttpGet("hello")]
public ActionResult<string> Hello() {
return new ActionResult<string>("Hello, World!");
}
```
2. Now instead of directly returning the `ActionResult`, return a `LogicResult`
```csharp
[HttpGet("hello")]
public ActionResult<string> Hello() {
return LogicResult<string>.Ok("Hello, World!");
}
```
3. This allows you to very easily change the return type by simply calling the right function
```csharp
[HttpGet("hello")]
public ActionResult<string> Hello() {
if (!Auth.IsLoggedIn)
return LogicResult<string>.Forbidden();
return LogicResult<string>.Ok("Hello, World!");
}
```
> **Hint:** You can also provide an error message for status codes that are not in the 200 range.

View File

@@ -1,79 +0,0 @@
# HopFrame Models
All models used by the RestAPI are listed below
## SingleValueResult
```csharp
public struct SingleValueResult<TValue>(TValue value) {
public TValue Value { get; set; } = value;
}
```
## UserPasswordValidation
```csharp
public sealed class UserPasswordValidation {
public string Password { get; set; }
}
```
## OpenIdConfiguration
```csharp
public sealed class OpenIdConfiguration {
public string Issuer { get; set; }
public string AuthorizationEndpoint { get; set; }
public string TokenEndpoint { get; set; }
public string UserinfoEndpoint { get; set; }
public string EndSessionEndpoint { get; set; }
public string IntrospectionEndpoint { get; set; }
public string RevocationEndpoint { get; set; }
public string DeviceAuthorizationEndpoint { get; set; }
public List<string> ResponseTypesSupported { get; set; }
public List<string> ResponseModesSupported { get; set; }
public string JwksUri { get; set; }
public List<string> GrantTypesSupported { get; set; }
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
public List<string> SubjectTypesSupported { get; set; }
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
public List<string> AcrValuesSupported { get; set; }
public List<string> ScopesSupported { get; set; }
public bool RequestParameterSupported { get; set; }
public List<string> ClaimsSupported { get; set; }
public bool ClaimsParameterSupported { get; set; }
public List<string> CodeChallengeMethodsSupported { get; set; }
}
```
## OpenIdIntrospection
```csharp
public sealed class OpenIdIntrospection {
public string Issuer { get; set; }
public string Subject { get; set; }
public string Audience { get; set; }
public long Expiration { get; set; }
public long IssuedAt { get; set; }
public long AuthTime { get; set; }
public string Acr { get; set; }
public List<string> AuthenticationMethods { get; set; }
public string SessionId { get; set; }
public string Email { get; set; }
public bool EmailVerified { get; set; }
public string Name { get; set; }
public string GivenName { get; set; }
public string PreferredUsername { get; set; }
public string Nickname { get; set; }
public List<string> Groups { get; set; }
public bool Active { get; set; }
public string Scope { get; set; }
public string ClientId { get; set; }
}
```
## OpenIdToken
```csharp
public sealed class OpenIdToken {
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
public string IdToken { get; set; }
}
```

View File

@@ -1,76 +0,0 @@
# HopFrame Authentication
HopFrame uses a token system with a short term access token and a long term refresh token for authenticating users.
These tokens are usually provided to the endpoints of the API / Blazor Pages through Cookies:
| Cookie key | Cookie value sample | Description |
|--------------------------------|----------------------------------------|-----------------------------|
| HopFrame.Security.RefreshToken | `42047983-914d-418b-841a-4382614231be` | The long term refresh token |
| HopFrame.Security.AccessToken | `d39c9432-0831-42df-8844-5e2b70f03eda` | The short term access token |
The advantage of these cookies is that they are automatically set by the backend and delete themselves, when they are
no longer valid.
The access token can also be delivered through a header called `HopFrame.Authentication` or `Token`.
It can also be delivered through a query parameter called `token`. This simplifies requests for images for example
because you can directly specify the url in the img tag in html.
## Authentication configuration
You can also configure the time span that the tokens are valid using the `appsettings.json` or environment variables
by configuring your configuration to load these.
>**Hint**: Configuring your application to use environment variables works by simply adding
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
> custom configurations / HopFrame services.
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
These get combined to a single time span. You can also completely disable the default authentication
by setting the `DefaultAuthentication` to `false`. Note that you will no longer be able to login in any
way unless you enabled the [OpenID](./openid.md) authentication.
#### Configuration example
```json
"HopFrame": {
"Authentication": {
"AccessToken": {
"Minutes": 30
},
"RefreshToken": {
"Days": 10,
"Hours": 5
}
}
}
```
#### Environment variables example
```dotenv
HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
```
## API tokens
API tokens are useful to use in automation environments that need to access an endpoint or page of your application.
The HopFrame supports this natively and no further configuration is required in order to use them.
### Create an api token
You can create an api token via the `ITokenRepository`:
```csharp
tokens.CreateApiToken(user, DateTime.MaxValue);
```
This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token
model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default
has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that an api token
can **never** have more permissions than the user associated with it.
### Add permissions to an api token
You can add permissions to an api token like you would to a normal user or group:
```csharp
permissions.AddPermission(apiToken, "token.permission");
```

View File

@@ -1,144 +0,0 @@
# HopFrame Admin Pages
Admin pages can be defined through a `AdminContext` similar to how a `DbContext` is defined. They generate administration pages like [`/administration/users`](./pages.md)
simply by reading the structure of the provided model and optionally some additional configuration.
> **Fun fact:** The already existing pages `/administration/users` and `/administration/groups` are also generated using an internal `AdminContext`.
## Usage
1. Create a class that inherits the `AdminPagesContext` base class
```csharp
public class AdminContext : AdminPagesContext {
}
```
2. Add your admin pages as properties to the class
```csharp
public class AdminContext : AdminPagesContext {
public AdminPage<Address> Addresses { get; set; }
public AdminPage<Employee> Employees { get; set; }
}
```
> **Hint:** you can specify the url of the admin page by adding the `AdminPageUrl` Attribute
3. **Optionally** you can further configure your pages in the `OnModelCreating` method
```csharp
public class AdminContext : AdminPagesContext {
public AdminPage<Address> Addresses { get; set; }
public AdminPage<Employee> Employees { get; set; }
public override void OnModelCreating(IAdminContextGenerator generator) {
base.OnModelCreating(generator);
generator.Page<Employee>()
.Property(e => e.Address)
.IsSelector();
generator.Page<Address>()
.Property(a => a.Employee)
.Ignore();
generator.Page<Address>()
.Property(a => a.AddressId)
.IsSelector<Employee>()
.Parser<Employee>((model, e) => model.AddressId = e.EmployeeId);
generator.Page<Employee>()
.ConfigureRepository<EmployeeProvider>()
.ListingProperty(e => e.Name);
generator.Page<Address>()
.ConfigureRepository<AddressProvider>()
.ListingProperty(a => a.City);
}
}
```
4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\
\
Attributes for classes and properties:
```csharp
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public sealed class AdminNameAttribute(string name) : Attribute {
public string Name { get; set; } = name;
}
```
Attributes for classes:
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
public bool ShowCreateButton { get; set; } = showCreateButton;
public bool ShowDeleteButton { get; set; } = showDeleteButton;
public bool ShowUpdateButton { get; set; } = showUpdateButton;
}
```
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
public AdminPagePermissions Permissions { get; set; } = new() {
Create = create,
Update = update,
Delete = delete,
View = view
};
}
```
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminDescriptionAttribute(string description) : Attribute {
public string Description { get; set; } = description;
}
```
Attributes for properties:
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminHideValueAttribute : Attribute;
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
public bool OnlyForListing { get; set; } = onlyForListing;
}
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
public string Prefix { get; set; } = prefix;
}
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUneditableAttribute : Attribute;
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public class AdminUniqueAttribute : Attribute;
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUnsortableAttribute : Attribute;
```
```csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class ListingPropertyAttribute : Attribute;
```

View File

@@ -1,20 +0,0 @@
# Auth Service
The `IAuthService` provides some useful methods to handle user authentication (login/register).
## Usage
Simply define the `IAuthService` as a dependency
```csharp
public interface IAuthService {
Task Register(UserRegister register);
Task<bool> Login(UserLogin login);
Task Logout();
Task<Token> RefreshLogin();
Task<bool> IsLoggedIn();
}
```
## Automatically refresh user sessions
1. Make sure you have implemented the `AuthMiddleware` how it's described in step 5 of the [installation](./installation.md).
2. After that, the access token of the user gets automatically refreshed as long as the refresh token is valid.

View File

@@ -1,20 +0,0 @@
# HopFrame Authentication
With the provided HopFrame services, you can secure your blazor pages so that only logged-in users or users with the right permissions can access the page.
## Usage
You can secure your Blazor pages by using the `AuthorizedView` component.
Everything placed inside this component will only be displayed if the authorization was successful.
You can also redirect the user if the authorization fails by specifying a `RedirectIfUnauthorized` url.
```html
<!-- You can either specify one 'Permission', multiple 'Permissions' or none if the user only needs to be logged-in -->
<AuthorizedView Permission="test.permission">
<p>This paragraph is only visible if the user is logged-in and has the required permission</p>
</AuthorizedView>
```
```html
<!-- This component will redirect the user to the login page if the user is unauthorized -->
<AuthorizedView RedirectIfUnauthorized="/login" />
```

View File

@@ -1,36 +0,0 @@
## How to use the Blazor API
This Installation adds all HopFrame [pages](./pages.md) and [repositories](../repositories.md) to the application.
1. Add the HopFrame.Web library to your project
```
dotnet add package HopFrame.Web
```
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
```csharp
builder.Services.AddHopFrame<DatabaseContext>();
```
4. **Optional:** You can also add your [AdminContext](./admin.md)
```csharp
builder.Services.AddAdminContext<AdminContext>();
```
5. Add the authentication middleware to your app
```csharp
app.UseMiddleware<AuthMiddleware>();
```
6. Add the HopFrame pages to your Razor components
```csharp
app.MapRazorComponents<App>()
.AddHopFrameAdminPages()
.AddInteractiveServerRenderMode();
```

View File

@@ -1,14 +0,0 @@
# HopFrame Pages
By default, the HopFrame provides some blazor pages for managing user accounts and permissions
## All currently supported blazor pages
| Page | Endpoint | Permission | Usage |
|-----------------|------------------------|----------------------------|--------------------------------------------------------------------------------------------------------|
| Admin Dashboard | /administration | hopframe.admin | This page provides an overview to all admin pages built-in and created by [AdminContexts](./admin.md). |
| Admin Login | /administration/login | | This page is a simple login screen so no login screen needs to be created to access the admin pages. |
| User Dashboard | /administration/users | hopframe.admin.users.view | This page serves as a management site for all users and their permissions. |
| Group Dashboard | /administration/groups | hopframe.admin.groups.view | This page serves as a management site for all groups and their permissions. |
> **Hint:** All pages created by [AdminContexts](./admin.md) are also under the `/administration/` location. This can unfortunately __not__ be changed at the moment.

View File

@@ -1,35 +0,0 @@
# Database initialization
You also need to initialize the data source with the tables from HopFrame.
## Create a DbContext
1. Create a c# class that inherits from the `HopDbContextBase` and add a data source (In the example Sqlite is used)\
**IMPORTANT:** You need to leave the `base.OnConfiguring(optionsBuilder)` in place so the HopFrame model relations are set correctly.
```csharp
public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("...");
}
}
```
2. Register the `DatabaseContext` as a service
```csharp
builder.Services.AddDbContext<DatabaseContext>();
```
3. Create a database migration
```bash
dotnet ef migrations add Initial
```
4. Apply the migration to the data source
```bash
dotnet ef database update
```

View File

@@ -1,97 +1,21 @@
# HopFrame base models # Models for HopFrame
All models listed below are part of the core HopFrame components and accessible in all installation variations
> **Note:** All properties of the models that are `virtual` are relational properties and don't directly correspond to columns in the database. This page shows all models that HopFrame uses.
## User
```csharp
public class User : IPermissionOwner {
public Guid Id { get; init; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime CreatedAt { get; set; }
public virtual List<Permission> Permissions { get; set; }
public virtual List<Token> Tokens { get; set; }
}
```
## PermissionGroup ## Base Models
```csharp These are the models used by the various database services.
public class PermissionGroup : IPermissionOwner {
public string Name { get; init; }
public bool IsDefaultGroup { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; set; }
public virtual List<Permission> Permissions { get; set; }
}
```
## Permission ![](./Diagrams/Models/img/BaseModels.svg)
```csharp
public class Permission {
public long Id { get; init; }
public string PermissionName { get; set; }
public DateTime GrantedAt { get; set; }
public virtual User User { get; set; }
public virtual PermissionGroup Group { get; set; }
public virtual Token Token { get; set; }
}
```
## Token
```csharp
public class Token : IPermissionOwner {
public int Type { get; set; }
public Guid Content { get; set; }
public DateTime CreatedAt { get; set; }
public virtual User Owner { get; set; }
public virtual List<Permission> Permissions { get; set; }
}
```
## UserLogin ## API Models
```csharp These are the models used by the REST API and the Blazor API.
public class UserLogin {
public string Email { get; set; }
public string Password { get; set; }
}
```
## UserRegister ![](./Diagrams/Models/img/ApiModels.svg)
```csharp
public class UserRegister {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
```
## UserCreator
```csharp
public class UserCreator {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual List<string> Permissions { get; set; }
}
```
## IPermissionOwner ## Database Models
```csharp These are the models that correspond to the scheme in the Database
public interface IPermissionOwner;
```
## SingleValueResult ![](./Diagrams/Models/img/DatabaseModels.svg)
```csharp
public struct SingleValueResult<TValue>(TValue value) {
public TValue Value { get; set; } = value;
}
```
## UserPasswordValidation
```csharp
public sealed class UserPasswordValidation {
public string Password { get; set; }
}
```

View File

@@ -1,120 +0,0 @@
# OpenID Authentication
The HopFrame allows you to use an OpenID provider as your authentication provider for single sign on or better security
etc. To use it, you just simply need to configure it through the `appsettings.json` or environment variables.
>**Note**: The Blazor module has not yet implemented endpoints for the login process, but the middleware is correctly
> configured and the `IOpenIdAccessor` service is also provided for you to easily implement the endpoints yourself.
When you have enabled the integration, new endpoints will also be provided to perform the authentication.
simply use the swagger explorer to look up how the endpoints function. They're all under the subroute
`/api/v1/openid/`.
## Configure the HopFrame to use OpenID authentication
1. Create / Configure your OpenID provider:
- Save the ClientID and Client Secret from the provider, because you need it later.
- The default redirect uri looks something like this: `https://example.com/api/v1/openid/callback`.
- **Replace** the origin with the FQDN of your service.
- In order for the HopFrame to automatically renew expired access tokens you need to enable the `offline_access` scope.
- The integration also works without doing that, but then you need to reauthenticate every time your access token expires.
2. Configure the HopFrame integration:
>**Hint**: All of these configuration options can also be defined as environment variables. Use '__'
> to separate the namespaces like so: `HOPFRAME__AUTHENTICATION__OPENID__ENABLED=true`
- Add the following lines to your `appsettings.json`:
```json
"HopFrame": {
"Authentication": {
"OpenID": {
"Enabled": true,
"Issuer": "your-issuer",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret"
}
}
}
```
>**Hint**: If you are using Authentik, the issuer url looks something like this: `https://auth.example.com/application/o/application-name/`.
> Just replace the FQDN and application-name with your configured application.
- **Optional**: You can also disable the default authentication via the config:
```json
"HopFrame": {
"Authentication": {
"DefaultAuthentication": false
}
}
```
- **Optional**: By default, the HopFrame will cache the api responses to reduce api latency. This can also be configured in the config (the cache can also be completely disabled here):
```json
"HopFrame": {
"Authentication": {
"OpenID": {
"Cache": {
"Enabled": true,
"Configuration": {
"Hours": 5
},
"Auth": {
"Seconds": 90
},
"Inspection": {
"Minutes": 5
}
}
}
}
}
```
- **Optional**: You can also define your own callback endpoint like so (you also need to add / replace the endpoint in the provider settings):
```json
"HopFrame": {
"Authentication": {
"OpenID": {
"Callback": "https://example.com/auth/callback"
}
}
}
```
- **Optional**: You can also prevent new users from being created by disabling it in the config:
```json
"HopFrame": {
"Authentication": {
"OpenID": {
"GenerateUsers": false
}
}
}
```
## Use the abstraction to integrate OpenID yourself
The HopFrame has a service, that simplifies the communication with the OpenID provider called `IOpenIdAccessor`.
You can inject it like every other service in your application.
```csharp
public interface IOpenIdAccessor {
Task<OpenIdConfiguration> LoadConfiguration();
Task<OpenIdToken> RequestToken(string code, string defaultCallback);
Task<string> ConstructAuthUri(string defaultCallback, string state = null);
Task<OpenIdIntrospection> InspectToken(string token);
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
}
```

View File

@@ -1,80 +0,0 @@
# HopFrame Permissions
Permissions in the HopFrame are simple and effective to use.
As discussed in the [repositories](./repositories.md) documentation, you can manage user / group permissions
via the `IPermissionRepository` service.
## How do permissions work in the HopFrame
Permissions are defined using the . (dot) syntax. This enables you to nest permissions in namespaces.
You can also give a user or a group the permission to every permission in a namespace by using the * (star) syntax.
| Permission | Example | Description |
|----------------------|-------------------------------|-------------------------------------------------------|
| `*` | `*` | all permissions |
| `[namespace].[name]` | `hopframe.admin.users.create` | single permission |
| `[namespace].*` | `hopframe.admin.*` | all permissions in that namespace (works recursively) |
### Reserved namespaces
| Namespace | Example | Description |
|-----------|---------------|------------------------------------------|
| `group` | `group.admin` | The user needs to be in a specific group |
### Permission Groups
You can manage them through the `IGroupRepository` as described in the [repositories](./repositories.md) documentation.
You add permissions just like you would to a user with the `IPermissionRepository`.
You can assign a user to a group by assigning the group permission to the user:
```csharp
permissionRepository.AddPermission(user, "group.admin");
```
## Predefined Permissions
| Permission | Description |
|--------------------------------|-------------------------------|
| `hopframe.admin` | Access to the admin dashboard |
| `hopframe.admin.users.read` | View all users |
| `hopframe.admin.users.update` | Edit a user |
| `hopframe.admin.users.delete` | Delete a user |
| `hopframe.admin.users.create` | Add a group |
| `hopframe.admin.groups.read` | View all groups |
| `hopframe.admin.groups.update` | Edit a group |
| `hopframe.admin.groups.delete` | Delete a group |
| `hopframe.admin.groups.create` | Add a group |
### Configuring HopFrame permissions
You can also configure the predefined permissions using the `appsettings.json` or environment variables
by configuring your configuration to load these.
>**Hint**: Configuring your application to use environment variables works by simply adding
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
> custom configurations / HopFrame services.
You can specify `Dashboard` for the dashboard permission and for `Users` and `Groups` you can specify
`Create`, `Read`, `Update` and `Delete` permissions.
#### Configuration example
```json
"HopFrame": {
"Permissions": {
"Dashboard": "myapp.dashboard.view",
"Users": {
"Read": "myapp.read.users"
},
"Groups": {
"Create": "myapp.create.groups",
"Update": "myapp.update.groups"
}
}
}
```
#### Environment variables example
```dotenv
HOPFRAME__PERMISSIONS__DASHBOARD="myapp.dashboard.view"
HOPFRAME__PERMISSIONS__USERS__READ="myapp.read.users"
HOPFRAME__PERMISSIONS__GROUPS__CREATE="myapp.create.groups"
HOPFRAME__PERMISSIONS__GROUPS__UPDATE="myapp.update.groups"
```

View File

@@ -1,28 +0,0 @@
# HopFrame Documentation
The HopFrame comes in two variations, you can eiter only use the backend with some basic endpoints or with fully fledged blazor pages for managing HopFrame components.
## Shared HopFrame Modules
- [Database](./database.md)
- [Repositories](./repositories.md)
- [Base Models](./models.md)
- [Authentication](./authentication.md)
- [Permissions](./permissions.md)
- [OpenID Integration](./openid.md)
## HopFrame Web API
- [Installation](./api/installation.md)
- [Endpoints](./api/endpoints.md)
- [Authorization](./api/authorization.md)
- [Models](./api/models.md)
- [LogicResults](./api/logicresults.md)
## HopFrame Blazor library
- [Installation](./blazor/installation.md)
- [Pages](./blazor/pages.md)
- [Authorization](./blazor/authorization.md)
- [Auth Service](./blazor/auth.md)
- [Admin Context](./blazor/admin.md)

View File

@@ -1,79 +0,0 @@
# HopFrame Repositories
The HopFrame provies repositories for the various build in database models as an abstraction around the `HopDbContext` to ensure, that the data is proccessed and saved correctly.
## Overview
The repositories can also be used by simply defining them as a dependency in your service / controller.
### User Repository
```csharp
public interface IUserRepository {
Task<IList<User>> GetUsers();
Task<User> GetUser(Guid userId);
Task<User> GetUserByEmail(string email);
Task<User> GetUserByUsername(string username);
Task<User> AddUser(User user);
Task UpdateUser(User user);
Task DeleteUser(User user);
Task<bool> CheckUserPassword(User user, string password);
Task ChangePassword(User user, string password);
}
```
### Group Repository
```csharp
public interface IGroupRepository {
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<IList<PermissionGroup>> GetDefaultGroups();
Task<IList<PermissionGroup>> GetUserGroups(User user);
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
Task DeletePermissionGroup(PermissionGroup group);
}
```
### Permission Repository
```csharp
public interface IPermissionRepository {
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(IPermissionOwner owner, string permission);
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
}
```
### Token Repository
```csharp
public interface ITokenRepository {
Task<Token> GetToken(string content);
Task<Token> CreateToken(int type, User owner);
Task DeleteUserTokens(User owner);
Task DeleteToken(Token token);
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
}
```

145
docs/services.md Normal file
View File

@@ -0,0 +1,145 @@
# HopFrame Services
This page describes all services provided by the HopFrame.
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
## HopFrame.Security
### ITokenContext
This service provides the information given by the current request
```csharp
public interface ITokenContext {
bool IsAuthenticated { get; }
User User { get; }
Guid AccessToken { get; }
}
```
### IUserService
This service simplifies the data access of the user table in the database.
```csharp
public interface IUserService {
Task<IList<User>> GetUsers();
Task<User> GetUser(Guid userId);
Task<User> GetUserByEmail(string email);
Task<User> GetUserByUsername(string username);
Task<User> AddUser(UserRegister user);
Task UpdateUser(User user);
Task DeleteUser(User user);
Task<bool> CheckUserPassword(User user, string password);
Task ChangePassword(User user, string password);
}
```
### IPermissionService
This service handles all permission and group interactions with the data source.
```csharp
public interface IPermissionService {
Task<bool> HasPermission(string permission, Guid user);
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
Task RemoveGroupFromUser(User user, PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
Task DeletePermissionGroup(PermissionGroup group);
Task<Permission> GetPermission(string name, IPermissionOwner owner);
Task AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(Permission permission);
Task<string[]> GetFullPermissions(string user);
}
```
## HopFrame.Api
### LogicResult
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
```csharp
public class LogicResult : ILogicResult {
public static LogicResult Ok();
public static LogicResult BadRequest();
public static LogicResult BadRequest(string message);
public static LogicResult Forbidden();
public static LogicResult Forbidden(string message);
public static LogicResult NotFound();
public static LogicResult NotFound(string message);
public static LogicResult Conflict();
public static LogicResult Conflict(string message);
public static LogicResult Forward(LogicResult result);
public static LogicResult Forward<T>(ILogicResult<T> result);
public static implicit operator ActionResult(LogicResult v);
}
public class LogicResult<T> : ILogicResult<T> {
public static LogicResult<T> Ok();
public static LogicResult<T> Ok(T result);
...
}
```
### IAuthLogic
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
```csharp
public interface IAuthLogic {
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
Task<LogicResult<SingleValueResult<string>>> Authenticate();
Task<LogicResult> Logout();
Task<LogicResult> Delete(UserPasswordValidation validation);
}
```
## HopFrame.Web
### IAuthService
This service handles all the authentication like login or register. It properly creates all tokens so the user can be identified
```csharp
public interface IAuthService {
Task Register(UserRegister register);
Task<bool> Login(UserLogin login);
Task Logout();
Task<TokenEntry> RefreshLogin();
Task<bool> IsLoggedIn();
}
```

70
docs/usage.md Normal file
View File

@@ -0,0 +1,70 @@
# HopFrame Usage
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
## Ho to use the Web API version
1. Add the HopFrame.Api library to your project:
```
dotnet add package HopFrame.Api
```
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
```csharp
public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("...");
}
}
```
3. Add the DbContext and HopFrame to your services
```csharp
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
```
## How to use the Blazor API
1. Add the HopFrame.Web library to your project
```
dotnet add package HopFrame.Web
```
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
```csharp
public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("...");
}
}
```
3. Add the DbContext and HopFrame to your services
```csharp
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
```
4. Add the authentication middleware to your app
```csharp
app.UseMiddleware<AuthMiddleware>();
```
5. Add the HopFrame pages to your Razor components
```csharp
app.MapRazorComponents<App>()
.AddHopFrameAdminPages()
.AddInteractiveServerRenderMode();
```

View File

@@ -1,74 +0,0 @@
using HopFrame.Api.Logic;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace HopFrame.Api.Controller;
[ApiController, Route("api/v1/groups")]
public class GroupController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IGroupLogic groups) : ControllerBase {
private async Task<bool> AuthorizeRequest(string permission) {
return await perms.HasPermission(context.AccessToken, permission);
}
[HttpGet, Authorized]
public async Task<ActionResult<IList<PermissionGroup>>> GetGroups() {
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
return Unauthorized();
return await groups.GetGroups();
}
[HttpGet("default"), Authorized]
public async Task<ActionResult<IList<PermissionGroup>>> GetDefaultGroups() {
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
return Unauthorized();
return await groups.GetDefaultGroups();
}
[HttpGet("user/{userId}"), Authorized]
public async Task<ActionResult<IList<PermissionGroup>>> GetUserGroups(string userId) {
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
return Unauthorized();
return await groups.GetUserGroups(userId);
}
[HttpGet("{name}"), Authorized]
public async Task<ActionResult<PermissionGroup>> GetGroup(string name) {
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
return Unauthorized();
return await groups.GetGroup(name);
}
[HttpPost, Authorized]
public async Task<ActionResult<PermissionGroup>> CreateGroup([FromBody] PermissionGroup group) {
if (!await AuthorizeRequest(permissions.Value.Groups.Create))
return Unauthorized();
return await groups.CreateGroup(group);
}
[HttpPut, Authorized]
public async Task<ActionResult<PermissionGroup>> UpdateGroup([FromBody] PermissionGroup group) {
if (!await AuthorizeRequest(permissions.Value.Groups.Update))
return Unauthorized();
return await groups.UpdateGroup(group);
}
[HttpDelete("{name}"), Authorized]
public async Task<ActionResult> DeleteGroup(string name) {
if (!await AuthorizeRequest(permissions.Value.Groups.Delete))
return Unauthorized();
return await groups.DeleteGroup(name);
}
}

View File

@@ -1,67 +0,0 @@
using HopFrame.Api.Models;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Controller;
[ApiController, Route("api/v1/openid")]
public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
public const string DefaultCallback = "api/v1/openid/callback";
[HttpGet("redirect")]
public async Task<IActionResult> RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) {
var uri = await accessor.ConstructAuthUri(redirectAfter);
if (performRedirect == 1) {
return Redirect(uri);
}
return Ok(new SingleValueResult<string>(uri));
}
[HttpGet("callback")]
public async Task<IActionResult> Callback([FromQuery] string code, [FromQuery] string state) {
if (string.IsNullOrEmpty(code)) {
return BadRequest("Authorization code is missing");
}
var token = await accessor.RequestToken(code);
if (token is null) {
return Forbid("Authorization code is not valid");
}
accessor.SetAuthenticationCookies(token);
if (string.IsNullOrEmpty(state)) {
return Ok(new SingleValueResult<string>(token.AccessToken));
}
return Redirect(state.Replace("{token}", token.AccessToken));
}
[HttpGet("refresh")]
public async Task<IActionResult> Refresh() {
var refreshToken = Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(refreshToken))
return BadRequest("Refresh token not provided");
var token = await accessor.RefreshAccessToken(refreshToken);
if (token is null)
return Conflict("Refresh token not valid");
accessor.SetAuthenticationCookies(token);
return Ok(new SingleValueResult<string>(token.AccessToken));
}
[HttpDelete("logout")]
public IActionResult Logout() {
accessor.Logout();
return Ok();
}
}

View File

@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Controller; namespace HopFrame.Api.Controller;
[ApiController] [ApiController]
[Route("api/v1/auth")] [Route("authentication")]
public class AuthController(IAuthLogic auth) : ControllerBase { public class SecurityController(IAuthLogic auth) : ControllerBase {
[HttpPut("login")] [HttpPut("login")]
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) { public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {

View File

@@ -1,83 +0,0 @@
using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace HopFrame.Api.Controller;
[ApiController, Route("api/v1/users")]
public class UserController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IUserLogic logic) : ControllerBase {
private async Task<bool> AuthorizeRequest(string permission) {
return await perms.HasPermission(context.AccessToken, permission);
}
[HttpGet, Authorized]
public async Task<ActionResult<IList<User>>> GetUsers() {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUsers();
}
[HttpGet("{userId}"), Authorized]
public async Task<ActionResult<User>> GetUser(string userId) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUser(userId);
}
[HttpGet("username/{username}"), Authorized]
public async Task<ActionResult<User>> GetUserByUsername(string username) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUserByUsername(username);
}
[HttpGet("email/{email}"), Authorized]
public async Task<ActionResult<User>> GetUserByEmail(string email) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUserByEmail(email);
}
[HttpPost, Authorized]
public async Task<ActionResult<User>> CreateUser([FromBody] UserCreator user) {
if (!await AuthorizeRequest(permissions.Value.Users.Create))
return Unauthorized();
return await logic.CreateUser(user);
}
[HttpPut("{userId}"), Authorized]
public async Task<ActionResult<User>> UpdateUser(string userId, [FromBody] User user) {
if (!await AuthorizeRequest(permissions.Value.Users.Update))
return Unauthorized();
return await logic.UpdateUser(userId, user);
}
[HttpDelete("{userId}"), Authorized]
public async Task<ActionResult> DeleteUser(string userId) {
if (!await AuthorizeRequest(permissions.Value.Users.Delete))
return Unauthorized();
return await logic.DeleteUser(userId);
}
[HttpPut("{userId}/password"), Authorized]
public async Task<ActionResult> ChangePassword(string userId, [FromBody] UserPasswordChange passwordChange) {
if (context.User.Id.ToString() != userId && !await AuthorizeRequest(permissions.Value.Users.Update))
return Unauthorized();
return await logic.UpdatePassword(userId, passwordChange.OldPassword, passwordChange.NewPassword);
}
}

View File

@@ -83,4 +83,4 @@ public static class MvcExtensions {
return true; return true;
} }
} }
} }

View File

@@ -1,12 +1,9 @@
using HopFrame.Api.Controller; using HopFrame.Api.Controller;
using HopFrame.Api.Logic; using HopFrame.Api.Logic;
using HopFrame.Api.Logic.Implementation; using HopFrame.Api.Logic.Implementation;
using HopFrame.Api.Models;
using HopFrame.Database; using HopFrame.Database;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Authentication.OpenID;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -18,53 +15,22 @@ public static class ServiceCollectionExtensions {
/// Adds all HopFrame endpoints and services to the application /// Adds all HopFrame endpoints and services to the application
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase { public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
config ??= new(); services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
AddHopFrameNoEndpoints<TDbContext>(services);
var controllers = new List<Type>();
if (config.ExposeModelEndpoints)
controllers.AddRange([typeof(UserController), typeof(GroupController)]);
var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication");
if ((!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication")) && config.ExposeAuthEndpoints)
controllers.Add(typeof(AuthController));
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled") && config.ExposeAuthEndpoints) {
IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback;
controllers.Add(typeof(OpenIdController));
}
AddHopFrameNoEndpoints<TDbContext>(services, configuration, config);
services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
} }
/// <summary> /// <summary>
/// Adds all HopFrame services to the application /// Adds all HopFrame services to the application
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase { public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
config ??= new();
services.AddMvcCore().ConfigureApplicationPartManager(manager => {
var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", ""));
manager.ApplicationParts.Remove(endpoints);
});
services.AddSingleton(config);
services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic>(); services.AddScoped<IAuthLogic, AuthLogic<TDbContext>>();
services.AddScoped<IUserLogic, UserLogic>();
services.AddScoped<IGroupLogic, GroupLogic>();
services.AddHopFrameAuthentication(configuration, config); services.AddHopFrameAuthentication<TDbContext>();
} }
} }

View File

@@ -7,6 +7,7 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<PackageId>HopFrame.Api</PackageId> <PackageId>HopFrame.Api</PackageId>
<Version>1.1.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
@@ -21,10 +22,4 @@
<None Include="README.md" Pack="true" PackagePath="\"/> <None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>HopFrame.Tests.Api</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

@@ -8,18 +8,9 @@ public interface IAuthLogic {
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register); Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
/// <summary>
/// Reassures that the user has a valid refresh token and generates a new access token
/// </summary>
/// <returns>The newly generated access token</returns>
Task<LogicResult<SingleValueResult<string>>> Authenticate(); Task<LogicResult<SingleValueResult<string>>> Authenticate();
Task<LogicResult> Logout(); Task<LogicResult> Logout();
/// <summary>
/// Deletes the user account that called the endpoint if the provided password is correct
/// </summary>
/// <param name="validation">The password od the user</param>
/// <returns></returns>
Task<LogicResult> Delete(UserPasswordValidation validation); Task<LogicResult> Delete(UserPasswordValidation validation);
} }

View File

@@ -1,14 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Api.Logic;
public interface IGroupLogic {
Task<LogicResult<IList<PermissionGroup>>> GetGroups();
Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups();
Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string userId);
Task<LogicResult<PermissionGroup>> GetGroup(string name);
Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group);
Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group);
Task<LogicResult> DeleteGroup(string name);
}

View File

@@ -1,5 +1,4 @@
using System.Net; using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Logic; namespace HopFrame.Api.Logic;

View File

@@ -1,16 +0,0 @@
using HopFrame.Api.Models;
using HopFrame.Database.Models;
namespace HopFrame.Api.Logic;
public interface IUserLogic {
Task<LogicResult<IList<User>>> GetUsers();
Task<LogicResult<User>> GetUser(string id);
Task<LogicResult<User>> GetUserByUsername(string username);
Task<LogicResult<User>> GetUserByEmail(string email);
Task<LogicResult<User>> CreateUser(UserCreator user);
Task<LogicResult<User>> UpdateUser(string id, User user);
Task<LogicResult> DeleteUser(string id);
Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword);
}

View File

@@ -1,19 +1,18 @@
using HopFrame.Api.Models; using HopFrame.Api.Models;
using HopFrame.Database.Models; using HopFrame.Database;
using HopFrame.Database.Repositories; using HopFrame.Database.Models.Entries;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options; using Microsoft.EntityFrameworkCore;
namespace HopFrame.Api.Logic.Implementation; namespace HopFrame.Api.Logic.Implementation;
internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic { public class AuthLogic<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic where TDbContext : HopDbContextBase {
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) { public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
var user = await users.GetUserByEmail(login.Email); var user = await users.GetUserByEmail(login.Email);
if (user is null) if (user is null)
@@ -22,84 +21,107 @@ internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens,
if (!await users.CheckUserPassword(user, login.Password)) if (!await users.CheckUserPassword(user, login.Password))
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct"); return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var refreshToken = new TokenEntry {
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = options.Value.RefreshTokenTime, MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = options.Value.AccessTokenTime, MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString()); await context.Tokens.AddRangeAsync(refreshToken, accessToken);
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
} }
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) { public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
if (register.Password.Length < 8) if (register.Password.Length < 8)
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long"); return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
var allUsers = await users.GetUsers(); var allUsers = await users.GetUsers();
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered"); return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
var user = await users.AddUser(new User { var user = await users.AddUser(register);
Username = register.Username,
Email = register.Email,
Password = register.Password
});
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var refreshToken = new TokenEntry {
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { await context.Tokens.AddRangeAsync(refreshToken, accessToken);
MaxAge = options.Value.RefreshTokenTime, await context.SaveChangesAsync();
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = options.Value.AccessTokenTime, MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString()); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
} }
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() { public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType]; var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided"); return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
var token = await tokens.GetToken(refreshToken);
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
if (token is null) if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid"); return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.Type != Token.RefreshTokenType) if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token"); return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) var accessToken = new TokenEntry {
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired"); CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = token.UserId
};
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); await context.Tokens.AddAsync(accessToken);
await context.SaveChangesAsync();
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = options.Value.AccessTokenTime, MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString()); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
} }
public async Task<LogicResult> Logout() { public async Task<LogicResult> Logout() {
@@ -107,7 +129,19 @@ internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens,
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType]; var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
await tokens.DeleteUserTokens(tokenContext.User); return LogicResult.Conflict("access or refresh token not provided");
var tokenEntries = await context.Tokens.Where(token =>
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
.ToArrayAsync();
if (tokenEntries.Length != 2)
return LogicResult.NotFound("One or more of the provided tokens was not found");
context.Tokens.Remove(tokenEntries[0]);
context.Tokens.Remove(tokenEntries[1]);
await context.SaveChangesAsync();
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);

View File

@@ -1,66 +0,0 @@
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
namespace HopFrame.Api.Logic.Implementation;
internal sealed class GroupLogic(IGroupRepository groups, IUserRepository users) : IGroupLogic {
public async Task<LogicResult<IList<PermissionGroup>>> GetGroups() {
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetPermissionGroups());
}
public async Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups() {
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetDefaultGroups());
}
public async Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string id) {
if (!Guid.TryParse(id, out var userId))
return LogicResult<IList<PermissionGroup>>.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult<IList<PermissionGroup>>.NotFound("That user does not exist");
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetUserGroups(user));
}
public async Task<LogicResult<PermissionGroup>> GetGroup(string name) {
var group = await groups.GetPermissionGroup(name);
if (group is null)
return LogicResult<PermissionGroup>.NotFound("That group does not exist");
return LogicResult<PermissionGroup>.Ok(group);
}
public async Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group) {
if (group is null)
return LogicResult<PermissionGroup>.BadRequest("Provide a group");
if (!group.Name.StartsWith("group."))
return LogicResult<PermissionGroup>.BadRequest("Group names must start with 'group.'");
if (await groups.GetPermissionGroup(group.Name) != null)
return LogicResult<PermissionGroup>.Conflict("That group already exists");
return LogicResult<PermissionGroup>.Ok(await groups.CreatePermissionGroup(group));
}
public async Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group) {
if (await groups.GetPermissionGroup(group.Name) == null)
return LogicResult<PermissionGroup>.NotFound("That user does not exist");
await groups.EditPermissionGroup(group);
return LogicResult<PermissionGroup>.Ok(group);
}
public async Task<LogicResult> DeleteGroup(string name) {
var group = await groups.GetPermissionGroup(name);
if (group is null)
return LogicResult.NotFound("That group does not exist");
await groups.DeletePermissionGroup(group);
return LogicResult.Ok();
}
}

View File

@@ -1,105 +0,0 @@
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Claims;
namespace HopFrame.Api.Logic.Implementation;
internal sealed class UserLogic(IUserRepository users, ITokenContext context) : IUserLogic {
public async Task<LogicResult<IList<User>>> GetUsers() {
return LogicResult<IList<User>>.Ok(await users.GetUsers());
}
public async Task<LogicResult<User>> GetUser(string id) {
if (!Guid.TryParse(id, out var userId))
return LogicResult<User>.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> GetUserByUsername(string username) {
var user = await users.GetUserByUsername(username);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> GetUserByEmail(string email) {
var user = await users.GetUserByEmail(email);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> CreateUser(UserCreator user) {
var createdUser = new User {
Email = user.Email,
Username = user.Username,
Password = user.Password,
};
createdUser.Permissions = user.Permissions?.Select(p => new Permission {
GrantedAt = DateTime.Now,
PermissionName = p,
User = createdUser
}).ToList();
var newUser = await users.AddUser(createdUser);
if (newUser is null)
return LogicResult<User>.Conflict("That user already exists");
return LogicResult<User>.Ok(newUser);
}
public async Task<LogicResult<User>> UpdateUser(string id, User user) {
if (!Guid.TryParse(id, out var userId))
return LogicResult<User>.BadRequest("Invalid user id");
if (user.Id != userId)
return LogicResult<User>.Conflict("Cannot edit user with different user id");
if (await users.GetUser(userId) is null)
return LogicResult<User>.NotFound("That user does not exist");
await users.UpdateUser(user);
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult> DeleteUser(string id) {
if (!Guid.TryParse(id, out var userId))
return LogicResult.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult.NotFound("That user does not exist");
await users.DeleteUser(user);
return LogicResult.Ok();
}
public async Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword) {
if (!Guid.TryParse(id, out var userId))
return LogicResult.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult.NotFound("That user does not exist");
if (userId == context.User.Id && !await users.CheckUserPassword(user, oldPassword))
return LogicResult.Conflict("Old password is not correct");
await users.ChangePassword(user, newPassword);
return LogicResult.Ok();
}
}

View File

@@ -1,8 +0,0 @@
using HopFrame.Security.Models;
namespace HopFrame.Api.Models;
public class HopFrameApiModuleConfig : HopFrameConfig {
public bool ExposeModelEndpoints { get; set; } = true;
public bool ExposeAuthEndpoints { get; set; } = true;
}

View File

@@ -1,10 +1,5 @@
namespace HopFrame.Api.Models; namespace HopFrame.Api.Models;
/// <summary>
/// Useful for endpoints that only return a single int or string
/// </summary>
/// <param name="value">The value of the result</param>
/// <typeparam name="TValue">The type of the result</typeparam>
public struct SingleValueResult<TValue>(TValue value) { public struct SingleValueResult<TValue>(TValue value) {
public TValue Value { get; set; } = value; public TValue Value { get; set; } = value;

View File

@@ -1,8 +0,0 @@
namespace HopFrame.Api.Models;
public class UserCreator {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual List<string> Permissions { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace HopFrame.Api.Models;
public class UserPasswordChange {
public string OldPassword { get; set; }
public string NewPassword { get; set; }
}

View File

@@ -1,4 +1,100 @@
# HopFrame API module # HopFrame API module
This module contains some useful endpoints for user login / register management. This module contains some useful endpoints for user login / register management.
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame). ## Ho to use the Web API version
1. Add the HopFrame.Api library to your project:
```
dotnet add package HopFrame.Api
```
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
```csharp
public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("...");
}
}
```
3. Add the DbContext and HopFrame to your services
```csharp
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
```
# Endpoints
By default, the module provides a controller for handling authentication based requests by the user.
You can explore the contoller by the build in swagger site from ASP .NET.
## Disable the Endpoints
```csharp
builder.Services.AddDbContext<DatabaseContext>();
//builder.Services.AddHopFrame<DatabaseContext>();
services.AddHopFrameNoEndpoints<TDbContext>();
```
# Services added in this module
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
## LogicResult
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
```csharp
public class LogicResult : ILogicResult {
public static LogicResult Ok();
public static LogicResult BadRequest();
public static LogicResult BadRequest(string message);
public static LogicResult Forbidden();
public static LogicResult Forbidden(string message);
public static LogicResult NotFound();
public static LogicResult NotFound(string message);
public static LogicResult Conflict();
public static LogicResult Conflict(string message);
public static LogicResult Forward(LogicResult result);
public static LogicResult Forward<T>(ILogicResult<T> result);
public static implicit operator ActionResult(LogicResult v);
}
public class LogicResult<T> : ILogicResult<T> {
public static LogicResult<T> Ok();
public static LogicResult<T> Ok(T result);
...
}
```
## IAuthLogic
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
```csharp
public interface IAuthLogic {
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
Task<LogicResult<SingleValueResult<string>>> Authenticate();
Task<LogicResult> Logout();
Task<LogicResult> Delete(UserPasswordValidation validation);
}
```

View File

@@ -1,4 +1,4 @@
using HopFrame.Database.Models; using HopFrame.Database.Models.Entries;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database; namespace HopFrame.Database;
@@ -8,66 +8,25 @@ namespace HopFrame.Database;
/// </summary> /// </summary>
public abstract class HopDbContextBase : DbContext { public abstract class HopDbContextBase : DbContext {
public static IList<Action<HopDbContextBase>> SaveHandlers = new List<Action<HopDbContextBase>>(); public virtual DbSet<UserEntry> Users { get; set; }
public virtual DbSet<PermissionEntry> Permissions { get; set; }
public virtual DbSet<User> Users { get; set; } public virtual DbSet<TokenEntry> Tokens { get; set; }
public virtual DbSet<Permission> Permissions { get; set; } public virtual DbSet<GroupEntry> Groups { get; set; }
public virtual DbSet<Token> Tokens { get; set; }
public virtual DbSet<PermissionGroup> Groups { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>() modelBuilder.Entity<UserEntry>();
.HasMany(u => u.Tokens) modelBuilder.Entity<PermissionEntry>();
.WithOne(t => t.Owner) modelBuilder.Entity<TokenEntry>();
.OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<GroupEntry>();
modelBuilder.Entity<User>()
.HasMany(u => u.Permissions)
.WithOne(p => p.User)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PermissionGroup>()
.HasMany(g => g.Permissions)
.WithOne(p => p.Group)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Token>()
.HasMany(t => t.Permissions)
.WithOne(t => t.Token)
.OnDelete(DeleteBehavior.Cascade);
} }
private void OnSaving() { /// <summary>
var orphanedPermissions = Permissions /// Gets executed when a user is deleted through the IUserService from the
.Where(p => p.UserId == null && p.GroupName == null && p.TokenId == null) /// HopFrame.Security package. You can override this method to also delete
.ToList(); /// related user specific entries in the database
/// </summary>
foreach (var handler in SaveHandlers) { /// <param name="user"></param>
handler.Invoke(this); public virtual void OnUserDelete(UserEntry user) {}
}
Permissions.RemoveRange(orphanedPermissions);
}
public override int SaveChanges() {
OnSaving();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
OnSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) {
OnSaving();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken()) {
OnSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
} }

View File

@@ -7,13 +7,13 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<PackageId>HopFrame.Database</PackageId> <PackageId>HopFrame.Database</PackageId>
<Version>1.1.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup> </ItemGroup>
@@ -21,10 +21,4 @@
<None Include="README.md" Pack="true" PackagePath="\"/> <None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>HopFrame.Tests.Database</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace HopFrame.Database.Models.Entries;
public class GroupEntry {
[Key, Required, MaxLength(50)]
public string Name { get; set; }
[Required, DefaultValue(false)]
public bool Default { get; set; }
[MaxLength(500)]
public string Description { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace HopFrame.Database.Models.Entries;
public sealed class PermissionEntry {
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long RecordId { get; set; }
[Required, MaxLength(255)]
public string PermissionText { get; set; }
[Required, MinLength(36), MaxLength(36)]
public string UserId { get; set; }
[Required]
public DateTime GrantedAt { get; set; }
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
namespace HopFrame.Database.Models.Entries;
public class TokenEntry {
public const int RefreshTokenType = 0;
public const int AccessTokenType = 1;
/// <summary>
/// Defines the Type of the stored Token
/// 0: Refresh token
/// 1: Access token
/// </summary>
[Required, MinLength(1), MaxLength(1)]
public int Type { get; set; }
[Key, Required, MinLength(36), MaxLength(36)]
public string Token { get; set; }
[Required, MinLength(36), MaxLength(36)]
public string UserId { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace HopFrame.Database.Models.Entries;
public class UserEntry {
[Key, Required, MinLength(36), MaxLength(36)]
public string Id { get; set; }
[MaxLength(50)]
public string Username { get; set; }
[Required, MaxLength(50), EmailAddress]
public string Email { get; set; }
[Required, MinLength(8), MaxLength(255)]
public string Password { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,56 @@
using HopFrame.Database.Models.Entries;
namespace HopFrame.Database.Models;
public static class ModelExtensions {
/// <summary>
/// Converts the database model to a friendly user model
/// </summary>
/// <param name="entry">the database model</param>
/// <param name="contextBase">the data source for the permissions and users</param>
/// <returns></returns>
public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) {
var user = new User {
Id = Guid.Parse(entry.Id),
Username = entry.Username,
Email = entry.Email,
CreatedAt = entry.CreatedAt
};
user.Permissions = contextBase.Permissions
.Where(perm => perm.UserId == entry.Id)
.Select(perm => perm.ToPermissionModel())
.ToList();
return user;
}
public static Permission ToPermissionModel(this PermissionEntry entry) {
Guid.TryParse(entry.UserId, out var userId);
return new Permission {
Owner = userId,
PermissionName = entry.PermissionText,
GrantedAt = entry.GrantedAt,
Id = entry.RecordId
};
}
public static PermissionGroup ToPermissionGroup(this GroupEntry entry, HopDbContextBase contextBase) {
var group = new PermissionGroup {
Name = entry.Name,
IsDefaultGroup = entry.Default,
Description = entry.Description,
CreatedAt = entry.CreatedAt
};
group.Permissions = contextBase.Permissions
.Where(perm => perm.UserId == group.Name)
.Select(perm => perm.ToPermissionModel())
.ToList();
return group;
}
}

View File

@@ -1,35 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public class Permission { public sealed class Permission {
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; init; } public long Id { get; init; }
[Required, MaxLength(255)]
public string PermissionName { get; set; } public string PermissionName { get; set; }
public Guid Owner { get; set; }
[Required]
public DateTime GrantedAt { get; set; } public DateTime GrantedAt { get; set; }
[ForeignKey("UserId"), JsonIgnore]
public virtual User User { get; set; }
public Guid? UserId { get; set; }
[ForeignKey("GroupName"), JsonIgnore]
public virtual PermissionGroup Group { get; set; }
[MaxLength(255)]
public string GroupName { get; set; }
[ForeignKey("TokenId"), JsonIgnore]
public virtual Token Token { get; set; }
public Guid? TokenId { get; set; }
} }
public interface IPermissionOwner; public interface IPermissionOwner {}

View File

@@ -1,22 +1,9 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public class PermissionGroup : IPermissionOwner { public class PermissionGroup : IPermissionOwner {
[Key, Required, MaxLength(50)]
public string Name { get; init; } public string Name { get; init; }
[Required, DefaultValue(false)]
public bool IsDefaultGroup { get; set; } public bool IsDefaultGroup { get; set; }
[MaxLength(500)]
public string Description { get; set; } public string Description { get; set; }
[Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public IList<Permission> Permissions { get; set; }
public virtual List<Permission> Permissions { get; set; }
} }

View File

@@ -1,36 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace HopFrame.Database.Models;
public class Token : IPermissionOwner {
public const int RefreshTokenType = 0;
public const int AccessTokenType = 1;
public const int ApiTokenType = 2;
public const int OpenIdTokenType = 3;
/// <summary>
/// Defines the Type of the stored Token
/// 0: Refresh token
/// 1: Access token
/// 2: Api token
/// </summary>
[Required, MinLength(1), MaxLength(1)]
public int Type { get; set; }
[Key, Required, MinLength(36), MaxLength(36)]
public Guid TokenId { get; set; }
/// <summary>
/// Defines the creation date of the token
/// In case of an api token it defines the date it becomes invalid
/// </summary>
[Required]
public DateTime CreatedAt { get; set; }
[ForeignKey("UserId"), JsonIgnore]
public virtual User Owner { get; set; }
public virtual List<Permission> Permissions { get; set; }
}

View File

@@ -1,28 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public class User : IPermissionOwner { public sealed class User : IPermissionOwner {
[Key, Required]
public Guid Id { get; init; } public Guid Id { get; init; }
[Required, MaxLength(50)]
public string Username { get; set; } public string Username { get; set; }
[Required, MaxLength(50), EmailAddress]
public string Email { get; set; } public string Email { get; set; }
[MinLength(8), MaxLength(255), JsonIgnore]
public string Password { get; set; }
[Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public IList<Permission> Permissions { get; set; }
public virtual List<Permission> Permissions { get; set; }
[JsonIgnore]
public virtual List<Token> Tokens { get; set; }
} }

View File

@@ -1,4 +1,2 @@
# HopFrame Database module # HopFrame Database module
This module contains all the logic for the database communication. This module contains all the logic for the database communication
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).

View File

@@ -1,21 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface IGroupRepository {
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<IList<PermissionGroup>> GetDefaultGroups();
Task<IList<PermissionGroup>> GetUserGroups(User user);
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
Task DeletePermissionGroup(PermissionGroup group);
internal Task<IList<string>> GetFullGroupPermissions(string group);
}

View File

@@ -1,23 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface IPermissionRepository {
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="owner"></param>
/// <param name="permission"></param>
/// <returns></returns>
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(IPermissionOwner owner, string permission);
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
}

View File

@@ -1,11 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface ITokenRepository {
Task<Token> GetToken(string content);
Task<Token> CreateToken(int type, User owner);
Task DeleteUserTokens(User owner, bool includeApiTokens = false);
Task DeleteToken(Token token);
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
}

View File

@@ -1,98 +0,0 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase {
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
return await context.Groups
.Include(g => g.Permissions)
.ToListAsync();
}
public async Task<IList<PermissionGroup>> GetDefaultGroups() {
return await context.Groups
.Include(g => g.Permissions)
.Where(g => g.IsDefaultGroup)
.ToListAsync();
}
public Task<IList<PermissionGroup>> GetUserGroups(User user) {
return Task.FromResult((IList<PermissionGroup>) context.Groups
.Include(g => g.Permissions)
.AsEnumerable()
.Where(g => user.Permissions.Any(p => p.PermissionName == g.Name))
.ToList());
}
public async Task<PermissionGroup> GetPermissionGroup(string name) {
return await context.Groups
.Include(g => g.Permissions)
.Where(g => g.Name == name)
.SingleOrDefaultAsync();
}
public async Task EditPermissionGroup(PermissionGroup group) {
var orig = await context.Groups
.Include(g => g.Permissions) // Include related entities
.SingleOrDefaultAsync(g => g.Name == group.Name);
if (orig is null) return;
// Update the main entity's properties
orig.IsDefaultGroup = group.IsDefaultGroup;
orig.Description = group.Description;
// Update the permissions
foreach (var permission in group.Permissions) {
var existingPermission = orig.Permissions.FirstOrDefault(p => p.Id == permission.Id);
if (existingPermission != null) {
// Update existing permission
context.Entry(existingPermission).CurrentValues.SetValues(permission);
} else {
// Add new permission
orig.Permissions.Add(permission);
}
}
// Remove deleted permissions
foreach (var permission in orig.Permissions.ToList().Where(permission => group.Permissions.All(p => p.Id != permission.Id))) {
orig.Permissions.Remove(permission);
context.Permissions.Remove(permission); // Ensure it gets removed from the database
}
await context.SaveChangesAsync();
}
public async Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group) {
group.CreatedAt = DateTime.Now;
await context.Groups.AddAsync(group);
await context.SaveChangesAsync();
return group;
}
public async Task DeletePermissionGroup(PermissionGroup group) {
context.Groups.Remove(group);
await context.SaveChangesAsync();
}
public async Task<IList<string>> GetFullGroupPermissions(string group) {
var permissions = await context.Permissions
.Include(p => p.Group)
.Where(p => p.Group != null)
.Where(p => p.Group.Name == group)
.Select(p => p.PermissionName)
.ToListAsync();
var groups = permissions
.Where(p => p.StartsWith("group."))
.ToList();
foreach (var subgroup in groups) {
permissions.AddRange(await GetFullGroupPermissions(subgroup));
}
return permissions;
}
}

View File

@@ -1,118 +0,0 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
public async Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions) {
if (owner is Token { Type: Token.ApiTokenType } token) {
if (!await HasPermission(token.Owner, permissions)) return false;
}
var perms = (await GetFullPermissions(owner)).ToArray();
foreach (var permission in permissions) {
if (!PermissionValidator.IncludesPermission(permission, perms)) return false;
}
return true;
}
public async Task<Permission> AddPermission(IPermissionOwner owner, string permission) {
var entry = new Permission {
GrantedAt = DateTime.Now,
PermissionName = permission
};
if (owner is User user) {
entry.User = user;
}else if (owner is PermissionGroup group) {
entry.Group = group;
}else if (owner is Token token) {
if (token.Type != Token.ApiTokenType)
throw new ArgumentException("Only API tokens can have permissions!");
if (!await HasPermission(token.Owner, permission))
throw new ArgumentException("An api token cannot have more permissions than the owner has!");
entry.Token = token;
}
await context.Permissions.AddAsync(entry);
await context.SaveChangesAsync();
return entry;
}
public async Task RemovePermission(IPermissionOwner owner, string permission) {
Permission entry = null;
if (owner is User user) {
entry = await context.Permissions
.Include(p => p.User)
.Where(p => p.User != null)
.Where(p => p.User.Id == user.Id)
.Where(p => p.PermissionName == permission)
.SingleOrDefaultAsync();
}else if (owner is PermissionGroup group) {
entry = await context.Permissions
.Include(p => p.Group)
.Where(p => p.Group != null)
.Where(p =>p.Group.Name == group.Name)
.Where(p => p.PermissionName == permission)
.SingleOrDefaultAsync();
}else if (owner is Token token) {
entry = await context.Permissions
.Include(p => p.Token)
.Where(p => p.Token != null)
.Where(p => p.Token.TokenId == token.TokenId)
.Where(p => p.PermissionName == permission)
.SingleOrDefaultAsync();
}
if (entry is not null) {
context.Permissions.Remove(entry);
await context.SaveChangesAsync();
}
}
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
var permissions = new List<string>();
if (owner is Token token && token.Type != Token.ApiTokenType) {
owner = token.Owner;
}
if (owner is User user) {
var perms = await context.Permissions
.Include(p => p.User)
.Where(p => p.User != null)
.Where(p => p.User.Id == user.Id)
.ToListAsync();
permissions.AddRange(perms.Select(p => p.PermissionName));
}else if (owner is PermissionGroup group) {
var perms = await context.Permissions
.Include(p => p.Group)
.Where(p => p.Group != null)
.Where(p =>p.Group.Name == group.Name)
.ToListAsync();
permissions.AddRange(perms.Select(p => p.PermissionName));
}else if (owner is Token apiToken) {
var perms = await context.Permissions
.Include(p => p.Token)
.Where(p => p.Token != null)
.Where(p =>p.Token.TokenId == apiToken.TokenId)
.ToListAsync();
permissions.AddRange(perms.Select(p => p.PermissionName));
}
var groups = permissions
.Where(p => p.StartsWith("group."))
.ToList();
foreach (var group in groups) {
permissions.AddRange(await groupRepository.GetFullGroupPermissions(group));
}
return permissions;
}
}

View File

@@ -1,66 +0,0 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase {
public async Task<Token> GetToken(string content) {
var success = Guid.TryParse(content, out Guid guid);
if (!success) return null;
return await context.Tokens
.Include(t => t.Owner)
.Where(t => t.TokenId == guid)
.SingleOrDefaultAsync();
}
public async Task<Token> CreateToken(int type, User owner) {
var token = new Token {
CreatedAt = DateTime.Now,
TokenId = Guid.NewGuid(),
Type = type,
Owner = owner
};
await context.Tokens.AddAsync(token);
await context.SaveChangesAsync();
return token;
}
public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
var tokens = await context.Tokens
.Include(t => t.Owner)
.Where(t => t.Owner.Id == owner.Id)
.ToListAsync();
if (!includeApiTokens)
tokens = tokens
.Where(t => t.Type != Token.ApiTokenType)
.ToList();
context.Tokens.RemoveRange(tokens);
await context.SaveChangesAsync();
}
public async Task DeleteToken(Token token) {
context.Tokens.Remove(token);
await context.SaveChangesAsync();
}
public async Task<Token> CreateApiToken(User owner, DateTime expirationDate) {
var token = new Token {
CreatedAt = expirationDate,
TokenId = Guid.NewGuid(),
Type = Token.ApiTokenType,
Owner = owner
};
await context.Tokens.AddAsync(token);
await context.SaveChangesAsync();
return token;
}
}

View File

@@ -1,148 +0,0 @@
using System.Globalization;
using System.Text;
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase {
private IIncludableQueryable<User, IList<Token>> IncludeReferences() {
return context.Users
.Include(u => u.Permissions)
.Include(u => u.Tokens);
}
public async Task<IList<User>> GetUsers() {
return await IncludeReferences()
.ToListAsync();
}
public async Task<User> GetUser(Guid userId) {
return await IncludeReferences()
.Where(u => u.Id == userId)
.SingleOrDefaultAsync();
}
public async Task<User> GetUserByEmail(string email) {
return await IncludeReferences()
.Where(u => u.Email == email)
.SingleOrDefaultAsync();
}
public async Task<User> GetUserByUsername(string username) {
return await IncludeReferences()
.Where(u => u.Username == username)
.SingleOrDefaultAsync();
}
public async Task<User> AddUser(User user) {
if (await GetUserByEmail(user.Email) is not null) return null;
if (await GetUserByUsername(user.Username) is not null) return null;
var entry = new User {
Id = Guid.NewGuid(),
Email = user.Email,
Username = user.Username,
CreatedAt = DateTime.Now,
Permissions = user.Permissions ?? new List<Permission>(),
Tokens = user.Tokens
};
entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture)));
var defaultGroups = await groupRepository.GetDefaultGroups();
foreach (var group in defaultGroups) {
entry.Permissions.Add(new Permission {
PermissionName = group.Name,
GrantedAt = DateTime.Now
});
}
await context.Users.AddAsync(entry);
await context.SaveChangesAsync();
return entry;
}
public async Task UpdateUser(User user) {
var entry = await IncludeReferences()
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
if (entry is null) return;
// Update the main entity's properties
entry.Email = user.Email;
entry.Username = user.Username;
// Update Permissions
foreach (var permission in user.Permissions) {
var existingPermission = entry.Permissions.FirstOrDefault(p => p.Id == permission.Id);
if (existingPermission != null) {
// Update existing permission
context.Entry(existingPermission).CurrentValues.SetValues(permission);
} else {
// Add new permission
entry.Permissions.Add(permission);
}
}
// Remove deleted permissions
foreach (var permission in entry.Permissions.ToList().Where(permission => user.Permissions.All(p => p.Id != permission.Id))) {
entry.Permissions.Remove(permission);
context.Permissions.Remove(permission); // Ensure it gets removed from the database
}
// Update Tokens
foreach (var token in user.Tokens) {
var existingToken = entry.Tokens.FirstOrDefault(t => t.TokenId == token.TokenId);
if (existingToken != null) {
// Update existing token
context.Entry(existingToken).CurrentValues.SetValues(token);
} else {
// Add new token
entry.Tokens.Add(token);
}
}
// Remove deleted tokens
foreach (var token in entry.Tokens.ToList().Where(token => user.Tokens.All(t => t.TokenId != token.TokenId))) {
entry.Tokens.Remove(token);
context.Tokens.Remove(token); // Ensure it gets removed from the database
}
await context.SaveChangesAsync();
}
public async Task DeleteUser(User user) {
var entry = await context.Users
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
if (entry is null) return;
context.Users.Remove(entry);
await context.SaveChangesAsync();
}
public async Task<bool> CheckUserPassword(User user, string password) {
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
var entry = await context.Users
.Where(entry => entry.Id == user.Id)
.SingleOrDefaultAsync();
return entry.Password == hash;
}
public async Task ChangePassword(User user, string password) {
var entry = await context.Users
.Where(entry => entry.Id == user.Id)
.SingleOrDefaultAsync();
if (entry is null) return;
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
entry.Password = hash;
await context.SaveChangesAsync();
}
}

View File

@@ -1,18 +0,0 @@
using HopFrame.Database.Repositories;
using HopFrame.Database.Repositories.Implementation;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Database;
public static class ServiceCollectionExtensions {
public static IServiceCollection AddHopFrameRepositories<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddScoped<IGroupRepository, GroupRepository<TDbContext>>();
services.AddScoped<IPermissionRepository, PermissionRepository<TDbContext>>();
services.AddScoped<IUserRepository, UserRepository<TDbContext>>();
services.AddScoped<ITokenRepository, TokenRepository<TDbContext>>();
return services;
}
}

View File

@@ -0,0 +1,15 @@
namespace HopFrame.Security;
public static class AdminPermissions {
public const string IsAdmin = "hopframe.admin";
public const string ViewUsers = "hopframe.admin.users.view";
public const string EditUser = "hopframe.admin.users.edit";
public const string DeleteUser = "hopframe.admin.users.delete";
public const string AddUser = "hopframe.admin.users.add";
public const string ViewGroups = "hopframe.admin.groups.view";
public const string EditGroup = "hopframe.admin.groups.edit";
public const string DeleteGroup = "hopframe.admin.groups.delete";
public const string AddGroup = "hopframe.admin.groups.add";
}

View File

@@ -1,11 +1,10 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using HopFrame.Database.Models; using HopFrame.Database;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -14,89 +13,45 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication; namespace HopFrame.Security.Authentication;
public class HopFrameAuthentication( public class HopFrameAuthentication<TDbContext>(
IOptionsMonitor<AuthenticationSchemeOptions> options, IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
ISystemClock clock, ISystemClock clock,
ITokenRepository tokens, TDbContext context,
IPermissionRepository perms, IPermissionService perms)
IOptions<HopFrameAuthenticationOptions> tokenOptions, : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)
IOptions<OpenIdOptions> openIdOptions, where TDbContext : HopDbContextBase {
IUserRepository users,
IOpenIdAccessor accessor)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
public const string SchemeName = "HopFrame.Authentication"; public const string SchemeName = "HopCore.Authentication";
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
var accessToken = Request.Cookies[ITokenContext.AccessTokenType]; var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
var tokenEntry = await tokens.GetToken(accessToken); var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled && !Guid.TryParse(accessToken, out _)) {
var result = await accessor.InspectToken(accessToken);
if (result is null || !result.Active)
return AuthenticateResult.Fail("Invalid OpenID Connect token");
var email = result.Email;
if (string.IsNullOrEmpty(email))
return AuthenticateResult.Fail("OpenID user has no email associated to it");
var user = await users.GetUserByEmail(email);
if (user is null) {
if (!openIdOptions.Value.GenerateUsers)
return AuthenticateResult.Fail("OpenID user does not exist");
var username = result.PreferredUsername;
user = await users.AddUser(new User {
Email = email,
Username = username
});
}
var token = new Token {
Owner = user,
CreatedAt = DateTime.Now,
Type = Token.OpenIdTokenType
};
var identity = await GenerateClaims(token, perms);
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
}
if (!tokenOptions.Value.DefaultAuthentication)
return AuthenticateResult.Fail("HopFrame authentication scheme is disabled");
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
if (tokenEntry.Type == Token.ApiTokenType) {
if (tokenEntry.CreatedAt < DateTime.Now) return AuthenticateResult.Fail("The provided API Token is expired");
}else if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
if (tokenEntry.Owner is null) if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId))
return AuthenticateResult.Fail("The provided Access Token does not match any user"); return AuthenticateResult.Fail("The provided Access Token does not match any user");
var principal = await GenerateClaims(tokenEntry, perms);
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
}
public static async Task<ClaimsPrincipal> GenerateClaims(Token token, IPermissionRepository perms) {
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()), new(HopFrameClaimTypes.AccessTokenId, accessToken),
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString()) new(HopFrameClaimTypes.UserId, tokenEntry.UserId)
}; };
var permissions = await perms.GetFullPermissions(token); var permissions = await perms.GetFullPermissions(tokenEntry.UserId);
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal(); var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName)); principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
return principal; return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
} }
} }

View File

@@ -1,66 +1,32 @@
using HopFrame.Database; using HopFrame.Database;
using HopFrame.Database.Models;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Implementation;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Services;
using HopFrame.Security.Options; using HopFrame.Security.Services.Implementation;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
namespace HopFrame.Security.Authentication; namespace HopFrame.Security.Authentication;
public static class HopFrameAuthenticationExtensions { public static class HopFrameAuthenticationExtensions {
/// <summary> /// <summary>
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API /// Configures the WebApplication to use the authentication and authorization of the HopFrame API
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="service">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param> /// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection services, ConfigurationManager configuration, HopFrameConfig config = null) { public static IServiceCollection AddHopFrameAuthentication<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase {
config ??= new HopFrameConfig(); service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>();
services.AddSingleton(config); service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
services.AddScoped(typeof(ICacheProvider), config.CacheProvider); service.AddScoped<IUserService, UserService<TDbContext>>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ITokenContext, TokenContextImplementor>();
if (config.CacheProvider == typeof(MemoryCacheProvider))
services.AddMemoryCache();
services.AddHttpClient<OpenIdAccessor>(); service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {});
services.AddScoped<IOpenIdAccessor, OpenIdAccessor>(); service.AddAuthorization();
services.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
services.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
services.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
services.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
services.AddAuthorization();
HopDbContextBase.SaveHandlers.Add(context => {
var section = configuration.GetSection("HopFrame:Authentication");
var accessToken = section?.GetSection("AccessToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().AccessTokenTime;
var refreshToken = section?.GetSection("RefreshToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().RefreshTokenTime;
var now = DateTime.Now; return service;
var accessTokenExpiry = now - accessToken;
var refreshTokenExpiry = now - refreshToken;
var invalidTokens = context.Tokens
.Where(t =>
(t.Type == Token.AccessTokenType && t.CreatedAt < accessTokenExpiry) ||
(t.Type == Token.RefreshTokenType && t.CreatedAt < refreshTokenExpiry))
.ToList();
context.Tokens.RemoveRange(invalidTokens);
});
return services;
} }
} }

View File

@@ -1,24 +0,0 @@
using HopFrame.Security.Options;
namespace HopFrame.Security.Authentication;
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
public override string Position { get; } = "HopFrame:Authentication";
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : RefreshToken.ConstructTimeSpan;
public bool DefaultAuthentication { get; set; } = true;
public TokenTime AccessToken { get; set; }
public TokenTime RefreshToken { get; set; }
public class TokenTime {
public int Days { get; set; }
public int Hours { get; set; }
public int Minutes { get; set; }
public int Seconds { get; set; }
public TimeSpan ConstructTimeSpan => new(Days, Hours, Minutes, Seconds);
}
}

View File

@@ -1,6 +0,0 @@
namespace HopFrame.Security.Authentication.OpenID;
public interface ICacheProvider {
Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class;
Task Set<TItem>(string key, TItem value, TimeSpan ttl);
}

View File

@@ -1,15 +0,0 @@
using HopFrame.Security.Authentication.OpenID.Models;
namespace HopFrame.Security.Authentication.OpenID;
public interface IOpenIdAccessor {
public static string DefaultCallback;
Task<OpenIdConfiguration> LoadConfiguration();
Task<OpenIdToken> RequestToken(string code);
Task<string> ConstructAuthUri(string state = null);
Task<OpenIdIntrospection> InspectToken(string token);
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
void SetAuthenticationCookies(OpenIdToken token);
void Logout();
}

View File

@@ -1,18 +0,0 @@
using Microsoft.Extensions.Caching.Memory;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
public class MemoryCacheProvider(IMemoryCache cache) : ICacheProvider {
public Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class {
if (cache.TryGetValue(key, out var value)) {
return Task.FromResult(value as TItem);
}
return factory.Invoke();
}
public Task Set<TItem>(string key, TItem value, TimeSpan ttl) {
cache.Set(key, value, ttl);
return Task.CompletedTask;
}
}

View File

@@ -1,156 +0,0 @@
using System.Text.Json;
using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, ICacheProvider cache) : IOpenIdAccessor {
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
public Task<OpenIdConfiguration> LoadConfiguration() {
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) {
return cache.GetOrCreate(ConfigurationCacheKey, LoadConfigurationInCache);
}
return LoadConfigurationInCache();
}
internal async Task<OpenIdConfiguration> LoadConfigurationInCache() {
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/"));
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled)
await cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
return config;
}
public Task<OpenIdToken> RequestToken(string code) {
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) {
return cache.GetOrCreate(AuthCodeCacheKey + code, () => RequestTokenInCache(code));
}
return RequestTokenInCache(code);
}
internal async Task<OpenIdToken> RequestTokenInCache(string code) {
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", callback },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled)
await cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan);
return token;
}
public async Task<string> ConstructAuthUri(string state = null) {
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
var configuration = await LoadConfiguration();
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}";
}
public Task<OpenIdIntrospection> InspectToken(string token) {
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) {
return cache.GetOrCreate(TokenCacheKey + token, () => InspectTokenInCache(token));
}
return InspectTokenInCache(token);
}
internal async Task<OpenIdIntrospection> InspectTokenInCache(string token) {
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "token", token },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled)
await cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan);
return introspection;
}
public async Task<OpenIdToken> RefreshAccessToken(string refreshToken) {
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "grant_type", "refresh_token" },
{ "refresh_token", refreshToken },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
}
public void SetAuthenticationCookies(OpenIdToken token) {
if (token.AccessToken is not null)
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
HttpOnly = false,
Secure = true
});
if (token.RefreshToken is not null)
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions {
MaxAge = options.Value.RefreshToken.ConstructTimeSpan,
HttpOnly = false,
Secure = true
});
}
public void Logout() {
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.AccessTokenType);
}
}

View File

@@ -1,68 +0,0 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdConfiguration {
[JsonPropertyName("issuer")]
public string Issuer { get; set; }
[JsonPropertyName("authorization_endpoint")]
public string AuthorizationEndpoint { get; set; }
[JsonPropertyName("token_endpoint")]
public string TokenEndpoint { get; set; }
[JsonPropertyName("userinfo_endpoint")]
public string UserinfoEndpoint { get; set; }
[JsonPropertyName("end_session_endpoint")]
public string EndSessionEndpoint { get; set; }
[JsonPropertyName("introspection_endpoint")]
public string IntrospectionEndpoint { get; set; }
[JsonPropertyName("revocation_endpoint")]
public string RevocationEndpoint { get; set; }
[JsonPropertyName("device_authorization_endpoint")]
public string DeviceAuthorizationEndpoint { get; set; }
[JsonPropertyName("response_types_supported")]
public List<string> ResponseTypesSupported { get; set; }
[JsonPropertyName("response_modes_supported")]
public List<string> ResponseModesSupported { get; set; }
[JsonPropertyName("jwks_uri")]
public string JwksUri { get; set; }
[JsonPropertyName("grant_types_supported")]
public List<string> GrantTypesSupported { get; set; }
[JsonPropertyName("id_token_signing_alg_values_supported")]
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
[JsonPropertyName("subject_types_supported")]
public List<string> SubjectTypesSupported { get; set; }
[JsonPropertyName("token_endpoint_auth_methods_supported")]
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
[JsonPropertyName("acr_values_supported")]
public List<string> AcrValuesSupported { get; set; }
[JsonPropertyName("scopes_supported")]
public List<string> ScopesSupported { get; set; }
[JsonPropertyName("request_parameter_supported")]
public bool RequestParameterSupported { get; set; }
[JsonPropertyName("claims_supported")]
public List<string> ClaimsSupported { get; set; }
[JsonPropertyName("claims_parameter_supported")]
public bool ClaimsParameterSupported { get; set; }
[JsonPropertyName("code_challenge_methods_supported")]
public List<string> CodeChallengeMethodsSupported { get; set; }
}

View File

@@ -1,62 +0,0 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdIntrospection {
[JsonPropertyName("iss")]
public string Issuer { get; set; }
[JsonPropertyName("sub")]
public string Subject { get; set; }
[JsonPropertyName("aud")]
public string Audience { get; set; }
[JsonPropertyName("exp")]
public long Expiration { get; set; }
[JsonPropertyName("iat")]
public long IssuedAt { get; set; }
[JsonPropertyName("auth_time")]
public long AuthTime { get; set; }
[JsonPropertyName("acr")]
public string Acr { get; set; }
[JsonPropertyName("amr")]
public List<string> AuthenticationMethods { get; set; }
[JsonPropertyName("sid")]
public string SessionId { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
[JsonPropertyName("email_verified")]
public bool EmailVerified { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("given_name")]
public string GivenName { get; set; }
[JsonPropertyName("preferred_username")]
public string PreferredUsername { get; set; }
[JsonPropertyName("nickname")]
public string Nickname { get; set; }
[JsonPropertyName("groups")]
public List<string> Groups { get; set; }
[JsonPropertyName("active")]
public bool Active { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
}

View File

@@ -1,20 +0,0 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdToken {
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("id_token")]
public string IdToken { get; set; }
}

View File

@@ -1,53 +0,0 @@
using HopFrame.Security.Options;
namespace HopFrame.Security.Authentication.OpenID.Options;
public sealed class OpenIdOptions : OptionsFromConfiguration {
public override string Position { get; } = "HopFrame:Authentication:OpenID";
public bool Enabled { get; set; } = false;
public bool GenerateUsers { get; set; } = true;
public string Issuer { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Callback { get; set; }
public HopFrameAuthenticationOptions.TokenTime RefreshToken { get; set; } = new() {
Days = 30
};
public CachingOptions Cache { get; set; } = new() {
Enabled = true,
Configuration = new() {
Enabled = true,
TTL = new() {
Hours = 24
}
},
Auth = new() {
Enabled = true,
TTL = new() {
Seconds = 30
}
},
Inspection = new() {
Enabled = true,
TTL = new() {
Minutes = 2
}
}
};
public class CachingTypeOptions {
public bool Enabled { get; set; }
public HopFrameAuthenticationOptions.TokenTime TTL { get; set; }
}
public class CachingOptions {
public bool Enabled { get; set; }
public CachingTypeOptions Configuration { get; set; }
public CachingTypeOptions Auth { get; set; }
public CachingTypeOptions Inspection { get; set; }
}
}

View File

@@ -1,30 +0,0 @@
using HopFrame.Security.Options;
namespace HopFrame.Security.Authorization;
public class AdminPermissionOptions : OptionsFromConfiguration {
public override string Position { get; } = "HopFrame:Permissions";
public string Dashboard { get; set; } = "hopframe.admin";
public CrudPermission Users { get; set; } = new() {
Read = "hopframe.admin.users.read",
Update = "hopframe.admin.users.update",
Delete = "hopframe.admin.users.delete",
Create = "hopframe.admin.users.create"
};
public CrudPermission Groups { get; set; } = new() {
Read = "hopframe.admin.groups.read",
Update = "hopframe.admin.groups.update",
Delete = "hopframe.admin.groups.delete",
Create = "hopframe.admin.groups.create"
};
public class CrudPermission {
public string Create { get; set; }
public string Read { get; set; }
public string Update { get; set; }
public string Delete { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
using HopFrame.Database;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Authorization;

View File

@@ -1,4 +1,4 @@
namespace HopFrame.Database; namespace HopFrame.Security.Authorization;
public static class PermissionValidator { public static class PermissionValidator {

View File

@@ -20,5 +20,5 @@ public interface ITokenContext {
/// <summary> /// <summary>
/// The access token the user provided /// The access token the user provided
/// </summary> /// </summary>
Token AccessToken { get; } Guid AccessToken { get; }
} }

View File

@@ -1,19 +1,15 @@
using HopFrame.Database;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Claims; namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext { internal sealed class TokenContextImplementor<TDbContext>(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase {
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult(); public User User => context.Users
.SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())?
public Token AccessToken => options.Value.Enabled ? new Token { .ToUserModel(context);
Owner = User,
Type = Token.OpenIdTokenType, public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString());
CreatedAt = DateTime.Now
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
} }

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation; using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace HopFrame.Database; namespace HopFrame.Security;
public static class EncryptionManager { public static class EncryptionManager {

View File

@@ -8,6 +8,7 @@
<RootNamespace>HopFrame.Security</RootNamespace> <RootNamespace>HopFrame.Security</RootNamespace>
<PackageId>HopFrame.Security</PackageId> <PackageId>HopFrame.Security</PackageId>
<Version>1.1.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>

View File

@@ -1,7 +0,0 @@
using HopFrame.Security.Authentication.OpenID.Implementation;
namespace HopFrame.Security.Models;
public class HopFrameConfig {
public Type CacheProvider { get; set; } = typeof(MemoryCacheProvider);
}

View File

@@ -1,5 +0,0 @@
namespace HopFrame.Security.Options;
public abstract class OptionsFromConfiguration {
public abstract string Position { get; }
}

View File

@@ -1,19 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Security.Options;
public static class OptionsFromConfigurationExtensions {
public static void AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration) where T : OptionsFromConfiguration {
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
string position = optionsInstance?.Position;
if (position is null) {
throw new ArgumentException($"""Configuration "{typeof(T).Name}" has no position configured!""");
}
services.Configure((Action<T>)(options => {
IConfigurationSection section = configuration.GetSection(position);
section.Bind(options);
}));
}
}

View File

@@ -1,4 +1,74 @@
# HopFrame Security module # HopFrame Security module
this module contains all handlers for the login and register validation. It also checks the user permissions. this module contains all handlers for the login and register validation. It also checks the user permissions.
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame). # Services added in this module
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
## ITokenContext
This service provides the information given by the current request
```csharp
public interface ITokenContext {
bool IsAuthenticated { get; }
User User { get; }
Guid AccessToken { get; }
}
```
## IUserService
This service simplifies the data access of the user table in the database.
```csharp
public interface IUserService {
Task<IList<User>> GetUsers();
Task<User> GetUser(Guid userId);
Task<User> GetUserByEmail(string email);
Task<User> GetUserByUsername(string username);
Task<User> AddUser(UserRegister user);
Task UpdateUser(User user);
Task DeleteUser(User user);
Task<bool> CheckUserPassword(User user, string password);
Task ChangePassword(User user, string password);
}
```
## IPermissionService
This service handles all permission and group interactions with the data source.
```csharp
public interface IPermissionService {
Task<bool> HasPermission(string permission, Guid user);
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
Task RemoveGroupFromUser(User user, PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
Task DeletePermissionGroup(PermissionGroup group);
Task<Permission> GetPermission(string name, IPermissionOwner owner);
Task AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(Permission permission);
Task<string[]> GetFullPermissions(string user);
}
```

View File

@@ -0,0 +1,48 @@
using HopFrame.Database.Models;
namespace HopFrame.Security.Services;
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
public interface IPermissionService {
Task<bool> HasPermission(string permission, Guid user);
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
Task RemoveGroupFromUser(User user, PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
Task DeletePermissionGroup(PermissionGroup group);
Task<Permission> GetPermission(string name, IPermissionOwner owner);
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="owner"></param>
/// <param name="permission"></param>
/// <returns></returns>
Task AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(Permission permission);
Task<string[]> GetFullPermissions(string user);
}

View File

@@ -1,8 +1,9 @@
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security.Models;
namespace HopFrame.Database.Repositories; namespace HopFrame.Security.Services;
public interface IUserRepository { public interface IUserService {
Task<IList<User>> GetUsers(); Task<IList<User>> GetUsers();
Task<User> GetUser(Guid userId); Task<User> GetUser(Guid userId);
@@ -11,8 +12,13 @@ public interface IUserRepository {
Task<User> GetUserByUsername(string username); Task<User> GetUserByUsername(string username);
Task<User> AddUser(User user); Task<User> AddUser(UserRegister user);
/// <summary>
/// IMPORTANT:<br/>
/// This function does not add or remove any permissions to the user.
/// For that please use <see cref="IPermissionService"/>
/// </summary>
Task UpdateUser(User user); Task UpdateUser(User user);
Task DeleteUser(User user); Task DeleteUser(User user);

Some files were not shown because too many files have changed in this diff Show More