47 Commits

Author SHA1 Message Date
5c7e38aa40 Merge branch 'dev' into 'main'
Release v2.1.3

See merge request leon.hoppe/hopframe!11
2024-12-23 11:43:32 +00:00
11126e8080 Merge branch 'feature/moduleConfig' into 'dev'
Resolve "Module configuration"

See merge request leon.hoppe/hopframe!10
2024-12-23 11:40:41 +00:00
0b9766f7db Added logout function + increased default openid config caching time 2024-12-23 12:38:30 +01:00
849ad649a8 Fixed path combining issues + added cookie helper function 2024-12-23 12:32:12 +01:00
3031dda710 Removed implicit callback definition 2024-12-23 12:17:54 +01:00
73d89a241f Fixed pipeline 2024-12-23 12:03:45 +01:00
df68b6dbf8 properly combined OpenId callback uri 2024-12-23 11:55:56 +01:00
20684ca40a added admin login url customization 2024-12-23 11:33:16 +01:00
a57127af0e Merge branch 'dev' into 'main'
hotfix/pipeline

See merge request leon.hoppe/hopframe!9
2024-12-22 19:31:35 +00:00
20b82245d0 reworked ci pipeline 2024-12-22 20:30:40 +01:00
8db38183c2 Merge branch 'main' into 'dev'
Merge hotfix

See merge request leon.hoppe/hopframe!8
2024-12-22 19:04:58 +00:00
5898ea8188 Merge branch 'hotfix/ci' into 'main'
Removed unnecessary versions + fixed ci artifacts

See merge request leon.hoppe/hopframe!7
2024-12-22 18:39:13 +00:00
1bc48b0ba2 Removed unnecessary versions + fixed ci artifacts 2024-12-22 19:38:16 +01:00
2308e1520d Merge branch 'release/v2.1.0' into 'main'
Release/v2.1.0

See merge request leon.hoppe/hopframe!6
2024-12-22 18:24:17 +00:00
1ede337565 Updated ci pipeline 2024-12-22 19:23:22 +01:00
a7d2f8031e Merge branch 'feature/crud' into 'dev'
Resolve "Crud API endpoints"

See merge request leon.hoppe/hopframe!5
2024-12-22 18:13:36 +00:00
4aab011224 Fixed Database update problem + added group management endpoints 2024-12-22 18:08:05 +01:00
ae74745108 Added user management endpoints 2024-12-22 17:32:09 +01:00
401dfc9909 Merge branch 'feature/openid' into 'dev'
Resolve "OAuth"

See merge request leon.hoppe/hopframe!4
2024-12-22 14:17:05 +00:00
ffae1be340 added proper documentation for openid integration 2024-12-22 15:13:55 +01:00
bee771a30e finished OpenID integration 2024-12-22 14:28:49 +01:00
9b38a10797 Added all necessary api endpoints for OpenID 2024-12-22 10:55:24 +01:00
ba7584c771 Added OpenID authentication method 2024-12-21 22:35:04 +01:00
df89450745 Merge branch 'feature/tokens' into 'dev'
Resolve "API tokens"

See merge request leon.hoppe/hopframe!3
2024-12-21 16:36:12 +00:00
c6aca4baf6 secured api tokens against permission breaches 2024-12-21 17:35:11 +01:00
e47d4917df Added api key documentation + fixed tests 2024-12-21 17:13:18 +01:00
59c452ff73 updated application to check for contextual permissions 2024-12-21 16:55:20 +01:00
ba46147a74 Added API token functionality 2024-12-21 16:09:55 +01:00
c087dbdf2b Merge branch 'feature/config' into 'dev'
Resolve "Configuratable token times"

See merge request leon.hoppe/hopframe!2
2024-12-21 14:03:48 +00:00
92afc85dba added permission configuration 2024-12-21 14:59:04 +01:00
51c15eff4c Added environment variable example for authentication configuration 2024-12-21 14:12:54 +01:00
422fd6c677 Added authentication documentation to table of contents 2024-12-21 14:08:47 +01:00
88c8fe612d Added configuration wrappers, authentication options and authentication documentation 2024-12-21 14:04:49 +01:00
dce0471105 Merge branch 'feature/testing' into 'dev'
Feature/testing

See merge request leon.hoppe/hopframe!1
2024-12-21 12:12:17 +00:00
c4ee8bb1e0 Fixed mistake corrected in v2.0.1 2024-12-21 13:10:46 +01:00
7c835ea49b Started working on UnitTests for frontend 2024-12-11 21:29:03 +01:00
5f746e0bc1 Renamed testing projects 2024-12-10 16:55:36 +01:00
ee7bf1e204 Renamed test projects 2024-12-10 16:39:28 +01:00
4d91ce1819 Implemented HopFrame.Web tests 2024-12-10 16:30:46 +01:00
a4d1d3227b Created tests for HopFrame.Api 2024-12-09 18:41:51 +01:00
14c82f4f06 Implemented security module tests 2024-11-24 17:18:35 +01:00
fca6ef4fa6 Implemented all tests for database module 2024-11-24 16:01:33 +01:00
da45a84f61 Moved test project to correct folder 2024-11-24 12:47:23 +01:00
b7eca1937c Attempted to fix test workflow not being restored 2024-11-24 12:43:41 +01:00
85031de3c2 Started creating tests for database module 2024-11-24 12:40:51 +01:00
1897428d00 Reorganized project in solution folders 2024-11-24 10:42:33 +01:00
4a5855250c Merge branch 'release/v2.0.0' into 'dev'
Resolve "Prepare release v2.0.0"

Closes #23

See merge request leon.hoppe/HopFrame!12
2024-11-23 15:16:16 +00:00
120 changed files with 3929 additions and 187 deletions

View File

@@ -17,18 +17,23 @@ build:
artifacts:
paths:
- "**/bin/Release"
expire_in: 10 minutes
test:
stage: test
script:
- dotnet test --no-restore --verbosity normal
- dotnet test --verbosity normal
dependencies:
- build
publish:
stage: publish
script:
- dotnet pack -c Release -o .
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json; done
- 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:
- main
variables:
NUGET_API_KEY: $NUGET_API_KEY
- tags
dependencies:
- build
- test

View File

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

14
.idea/.idea.HopFrame/.idea/discord.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?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>

View File

@@ -2,7 +2,7 @@
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTest", "test\RestApiTest\RestApiTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
EndProject
@@ -10,10 +10,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Web", "testing\HopFrame.Testing.Web\HopFrame.Testing.Web.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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,7 +62,34 @@ Global
{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
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
EndGlobal

View File

@@ -1,8 +1,86 @@
<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_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_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;
&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;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>
&lt;/AssemblyExplorer&gt;</s:String>
</wpf:ResourceDictionary>

View File

@@ -6,6 +6,8 @@ A simple backend management api for ASP.NET Core Web APIs
- [x] User authentication
- [x] Permission management
- [x] Generated frontend administration boards
- [x] API token support
- [x] OpenID authentication integration
# Usage
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.

76
docs/authentication.md Normal file
View File

@@ -0,0 +1,76 @@
# 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

@@ -35,16 +35,18 @@ public class Permission {
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 {
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; }
}
```

120
docs/openid.md Normal file
View File

@@ -0,0 +1,120 @@
# 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);
}
```

80
docs/permissions.md Normal file
View File

@@ -0,0 +1,80 @@
# 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

@@ -7,6 +7,9 @@ The HopFrame comes in two variations, you can eiter only use the backend with so
- [Database](./database.md)
- [Repositories](./repositories.md)
- [Base Models](./models.md)
- [Authentication](./authentication.md)
- [Permissions](./permissions.md)
- [OpenID Integration](./openid.md)
## HopFrame Web API

View File

@@ -71,5 +71,9 @@ public interface ITokenRepository {
Task<Token> CreateToken(int type, User owner);
Task DeleteUserTokens(User owner);
Task DeleteToken(Token token);
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
}
```

View File

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

View File

@@ -0,0 +1,74 @@
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

@@ -0,0 +1,67 @@
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 NotFound("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

@@ -0,0 +1,83 @@
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;
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
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,4 +1,5 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Logic;

View File

@@ -0,0 +1,16 @@
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

@@ -5,12 +5,15 @@ using HopFrame.Security.Authentication;
using HopFrame.Security.Claims;
using HopFrame.Security.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Api.Logic.Implementation;
public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
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);
if (user is null)
@@ -22,23 +25,25 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = true,
Secure = true
});
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
}
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)
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
var allUsers = await users.GetUsers();
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
@@ -53,46 +58,48 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
}
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];
if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided");
var token = await tokens.GetToken(refreshToken);
if (token.Type != Token.RefreshTokenType)
return LogicResult<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
if (token.Type != Token.RefreshTokenType)
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token");
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
}
public async Task<LogicResult> Logout() {
@@ -100,9 +107,7 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return LogicResult.Conflict("access or refresh token not provided");
await tokens.DeleteUserTokens(tokenContext.User);
await tokens.DeleteUserTokens(tokenContext.User);
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);

View File

@@ -0,0 +1,66 @@
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

@@ -0,0 +1,105 @@
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

@@ -0,0 +1,8 @@
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

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

View File

@@ -30,5 +30,10 @@ public abstract class HopDbContextBase : DbContext {
.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);
}
}

View File

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

View File

@@ -21,6 +21,9 @@ public class Permission {
[ForeignKey("GroupName"), JsonIgnore]
public virtual PermissionGroup Group { get; set; }
[ForeignKey("TokenId"), JsonIgnore]
public virtual Token Token { get; set; }
}
public interface IPermissionOwner;

View File

@@ -4,24 +4,33 @@ using System.Text.Json.Serialization;
namespace HopFrame.Database.Models;
public class Token {
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 Content { get; set; }
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

@@ -5,7 +5,7 @@ namespace HopFrame.Database.Models;
public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)]
[Key, Required]
public Guid Id { get; init; }
[Required, MaxLength(50)]
@@ -14,7 +14,7 @@ public class User : IPermissionOwner {
[Required, MaxLength(50), EmailAddress]
public string Email { get; set; }
[Required, MinLength(8), MaxLength(255), JsonIgnore]
[MinLength(8), MaxLength(255), JsonIgnore]
public string Password { get; set; }
[Required]

View File

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

View File

@@ -33,19 +33,38 @@ internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRe
}
public async Task EditPermissionGroup(PermissionGroup group) {
var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name);
var orig = await context.Groups
.Include(g => g.Permissions) // Include related entities
.SingleOrDefaultAsync(g => g.Name == group.Name);
if (orig is null) return;
var entity = context.Groups.Update(orig);
// Update the main entity's properties
orig.IsDefaultGroup = group.IsDefaultGroup;
orig.Description = group.Description;
entity.Entity.IsDefaultGroup = group.IsDefaultGroup;
entity.Entity.Description = group.Description;
entity.Entity.Permissions = group.Permissions;
// 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);

View File

@@ -5,6 +5,10 @@ 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) {
@@ -24,6 +28,12 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
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);
@@ -48,6 +58,13 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
.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) {
@@ -58,6 +75,10 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
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
@@ -74,6 +95,14 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
.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));
}

View File

@@ -1,5 +1,6 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace HopFrame.Database.Repositories.Implementation;
@@ -11,14 +12,14 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
return await context.Tokens
.Include(t => t.Owner)
.Where(t => t.Content == guid)
.Where(t => t.TokenId == guid)
.SingleOrDefaultAsync();
}
public async Task<Token> CreateToken(int type, User owner) {
var token = new Token {
CreatedAt = DateTime.Now,
Content = Guid.NewGuid(),
TokenId = Guid.NewGuid(),
Type = type,
Owner = owner
};
@@ -29,13 +30,37 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
return token;
}
public async Task DeleteUserTokens(User owner) {
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

@@ -69,10 +69,45 @@ internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepos
.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;
entry.Permissions = user.Permissions;
entry.Tokens = user.Tokens;
// 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();
}

View File

@@ -1,15 +0,0 @@
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,6 +1,9 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
@@ -17,39 +20,83 @@ public class HopFrameAuthentication(
UrlEncoder encoder,
ISystemClock clock,
ITokenRepository tokens,
IPermissionRepository perms,
IOptions<HopFrameAuthenticationOptions> tokenOptions,
IOptions<OpenIdOptions> openIdOptions,
IUserRepository users,
IPermissionRepository perms)
IOpenIdAccessor accessor)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
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);
public const string SchemeName = "HopFrame.Authentication";
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
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");
var tokenEntry = await tokens.GetToken(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.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)
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> {
new(HopFrameClaimTypes.AccessTokenId, accessToken),
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
};
var permissions = await perms.GetFullPermissions(tokenEntry.Owner);
var permissions = await perms.GetFullPermissions(token);
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
return principal;
}
}

View File

@@ -1,23 +1,36 @@
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.Options;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace HopFrame.Security.Authentication;
public static class HopFrameAuthenticationExtensions {
/// <summary>
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
/// </summary>
/// <param name="service">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <returns></returns>
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) {
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddHttpClient();
service.AddMemoryCache();
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
service.AddAuthorization();

View File

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,145 @@
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.Caching.Memory;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor {
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
public async Task<OpenIdConfiguration> LoadConfiguration() {
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) {
return cachedConfiguration as OpenIdConfiguration;
}
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)
cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
return config;
}
public async Task<OpenIdToken> RequestToken(string code) {
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) {
return cachedToken as OpenIdToken;
}
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)
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 async Task<OpenIdIntrospection> InspectToken(string token) {
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) {
return cachedToken as OpenIdIntrospection;
}
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)
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

@@ -0,0 +1,68 @@
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

@@ -0,0 +1,62 @@
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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,53 @@
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

@@ -0,0 +1,30 @@
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,13 +1,19 @@
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext {
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext {
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 Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
public Token AccessToken => options.Value.Enabled ? new Token {
Owner = User,
Type = Token.OpenIdTokenType,
CreatedAt = DateTime.Now
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
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

@@ -8,6 +8,6 @@ public sealed class AdminPermissionsAttribute(string view = null, string create
Create = create,
Update = update,
Delete = delete,
View = view
Read = view
};
}

View File

@@ -24,7 +24,7 @@ public interface IAdminPageGenerator<TModel> {
/// </summary>
/// <param name="permission">the specified permission</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ViewPermission(string permission);
IAdminPageGenerator<TModel> ReadPermission(string permission);
/// <summary>
/// Sets the permission needed to create a new Entry

View File

@@ -48,8 +48,8 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
return this;
}
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
Page.Permissions.View = permission;
public IAdminPageGenerator<TModel> ReadPermission(string permission) {
Page.Permissions.Read = permission;
return this;
}
@@ -165,7 +165,7 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
CreatePermission(attribute?.Permissions.Create);
UpdatePermission(attribute?.Permissions.Update);
ViewPermission(attribute?.Permissions.View);
ReadPermission(attribute?.Permissions.Read);
DeletePermission(attribute?.Permissions.Delete);
}

View File

@@ -6,7 +6,6 @@
<Nullable>disable</Nullable>
<PackageId>HopFrame.Web.Admin</PackageId>
<Version>2.0.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable>

View File

@@ -1,7 +1,7 @@
namespace HopFrame.Web.Admin.Models;
public sealed class AdminPagePermissions {
public string View { get; set; }
public string Read { get; set; }
public string Create { get; set; }
public string Update { get; set; }
public string Delete { get; set; }

View File

@@ -1,7 +1,6 @@
using System.Security.Claims;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication;
using HopFrame.Security.Claims;
using HopFrame.Web.Services;
using Microsoft.AspNetCore.Http;
@@ -20,16 +19,10 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
next?.Invoke(context);
return;
}
var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
};
var permissions = await perms.GetFullPermissions(token.Owner);
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
var principal = await HopFrameAuthentication.GenerateClaims(token, perms);
if (principal?.Identity is ClaimsIdentity identity)
context.User.AddIdentity(identity);
}
await next?.Invoke(context);

View File

@@ -321,7 +321,7 @@
private async void Save() {
if (_isEdit && _currentPage.Permissions.Update is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Update)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to edit an entry!",
@@ -330,7 +330,7 @@
return;
}
}else if (_currentPage.Permissions.Create is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) {
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Create)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to add an entry!",

View File

@@ -1,15 +1,17 @@
using System.Text.RegularExpressions;
using HopFrame.Database.Models;
using HopFrame.Security;
using HopFrame.Security.Authorization;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
using HopFrame.Web.Provider;
using Microsoft.Extensions.Options;
namespace HopFrame.Web;
internal class HopAdminContext : AdminPagesContext {
internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : AdminPagesContext {
[AdminPageUrl("users")]
public AdminPage<User> Users { get; set; }
@@ -21,10 +23,10 @@ internal class HopAdminContext : AdminPagesContext {
generator.Page<User>()
.Description("On this page you can manage all user accounts.")
.ConfigureProvider<UserProvider>()
.ViewPermission(AdminPermissions.ViewUsers)
.CreatePermission(AdminPermissions.AddUser)
.UpdatePermission(AdminPermissions.EditUser)
.DeletePermission(AdminPermissions.DeleteUser);
.ReadPermission(options.Value.Users.Read)
.CreatePermission(options.Value.Users.Create)
.UpdatePermission(options.Value.Users.Update)
.DeletePermission(options.Value.Users.Delete);
generator.Page<User>().Property(u => u.Password)
.DisplayInListing(false)
@@ -64,10 +66,10 @@ internal class HopAdminContext : AdminPagesContext {
generator.Page<PermissionGroup>()
.Description("On this page you can view, create, edit and delete permission groups.")
.ConfigureProvider<GroupProvider>()
.ViewPermission(AdminPermissions.ViewGroups)
.CreatePermission(AdminPermissions.AddGroup)
.UpdatePermission(AdminPermissions.EditGroup)
.DeletePermission(AdminPermissions.DeleteGroup)
.ReadPermission(options.Value.Groups.Read)
.CreatePermission(options.Value.Groups.Create)
.UpdatePermission(options.Value.Groups.Update)
.DeletePermission(options.Value.Groups.Delete)
.ListingProperty(g => g.Name);
generator.Page<PermissionGroup>().Property(g => g.Name)

View File

@@ -7,7 +7,6 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PackageId>HopFrame.Web</PackageId>
<Version>2.0.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable>
@@ -35,4 +34,10 @@
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>HopFrame.Tests.Web</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,5 @@
namespace HopFrame.Web.Models;
public class HopFrameWebModuleConfig {
public string AdminLoginPageUri { get; set; } = "/administration/login";
}

View File

@@ -5,25 +5,27 @@
@using BlazorStrap
@using HopFrame.Web.Pages.Administration.Layout
@using BlazorStrap.V5
@using HopFrame.Security
@using HopFrame.Security.Authorization
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Components
@using HopFrame.Web.Models
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Options
@layout AdminLayout
<AuthorizedView Permission="@AdminPermissions.IsAdmin" RedirectIfUnauthorized="/administration/login" />
<AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="@ConstructRedirectUri()" />
<PageTitle>Admin Dashboard</PageTitle>
<BSContainer>
<BSRow Justify="Justify.Center">
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View">
<AuthorizedView Permission="@adminPage.Permissions.Read">
<BSCol Column="4" style="margin-bottom: 10px">
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px; min-width: 200px">
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.Read</span></BSCard>
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
</BSCard>
@@ -36,11 +38,17 @@
@inject NavigationManager Navigator
@inject IAdminPagesProvider Pages
@inject IOptions<AdminPermissionOptions> Options
@inject HopFrameWebModuleConfig Config
@code {
public void NavigateTo(string url) {
Navigator.NavigateTo("administration/" + url, true);
Navigator.NavigateTo("/administration/" + url, true);
}
public string ConstructRedirectUri() {
return Config.AdminLoginPageUri + "?redirect=/administration";
}
}

View File

@@ -43,7 +43,7 @@
private UserLogin UserLogin { get; set; }
[SupplyParameterFromQuery(Name = "redirect")]
private string RedirectAfter { get; set; }
public string RedirectAfter { get; set; }
private const string DefaultRedirect = "/administration";
@@ -65,6 +65,6 @@
return;
}
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : "/administration/" + RedirectAfter, true);
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true);
}
}

View File

@@ -16,9 +16,10 @@
@using HopFrame.Security.Claims
@using HopFrame.Web.Admin
@using HopFrame.Web.Components
@using HopFrame.Web.Models
<PageTitle>@_pageData.Title</PageTitle>
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="@GenerateRedirectString()" />
<AuthorizedView Permission="@_pageData.Permissions.Read" RedirectIfUnauthorized="@GenerateRedirectString()" />
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>
@@ -33,7 +34,7 @@
<div class="d-flex" role="search" id="search">
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @oninput="TriggerSearch">
</div>
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
<AuthorizedView Permission="@_pageData.Permissions.Create">
<BSButton IsSubmit="false" Color="BSColor.Success" @onclick="Create">Add Entry</BSButton>
</AuthorizedView>
</div>
@@ -107,6 +108,7 @@
@inject IPermissionRepository Permissions
@inject SweetAlertService Alerts
@inject NavigationManager Navigator
@inject HopFrameWebModuleConfig Config
@code {
[Parameter]
@@ -140,8 +142,8 @@
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelProvider = _pageData.LoadModelProvider(Provider);
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete);
await Reload();
}
@@ -251,6 +253,6 @@
}
private string GenerateRedirectString() {
return "/administration/login?redirect=" + _pageData?.Url;
return Config.AdminLoginPageUri + "?redirect=/administration/" + _pageData?.Url;
}
}

View File

@@ -24,7 +24,7 @@
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View">
<AuthorizedView Permission="@adminPage.Permissions.Read">
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
</AuthorizedView>
}

View File

@@ -3,26 +3,29 @@ using CurrieTechnologies.Razor.SweetAlert2;
using HopFrame.Database;
using HopFrame.Security.Authentication;
using HopFrame.Web.Admin;
using HopFrame.Web.Models;
using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Web;
public static class ServiceCollectionExtensions {
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase {
services.AddHttpClient();
services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>();
services.AddTransient<AuthMiddleware>();
services.AddAdminContext<HopAdminContext>();
services.AddSingleton(config ?? new HopFrameWebModuleConfig());
// Component library's
services.AddSweetAlert2();
services.AddBlazorStrap();
services.AddHopFrameAuthentication();
services.AddHopFrameAuthentication(configuration);
return services;
}

View File

@@ -1,9 +1,12 @@
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims;
using HopFrame.Security.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Web.Services.Implementation;
@@ -11,10 +14,16 @@ internal class AuthService(
IUserRepository userService,
IHttpContextAccessor httpAccessor,
ITokenRepository tokens,
ITokenContext context)
ITokenContext context,
IOptions<HopFrameAuthenticationOptions> options,
IOptions<OpenIdOptions> openIdOptions,
IOpenIdAccessor accessor,
IUserRepository users)
: IAuthService {
public async Task Register(UserRegister register) {
if (!options.Value.DefaultAuthentication) return;
var user = await userService.AddUser(new User {
Username = register.Username,
Email = register.Email,
@@ -26,19 +35,21 @@ internal class AuthService(
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
}
public async Task<bool> Login(UserLogin login) {
if (!options.Value.DefaultAuthentication) return false;
var user = await userService.GetUserByEmail(login.Email);
if (user == null) return false;
@@ -47,13 +58,13 @@ internal class AuthService(
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
@@ -73,16 +84,51 @@ internal class AuthService(
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
if (openIdOptions.Value.Enabled && !Guid.TryParse(refreshToken, out _)) {
var openIdToken = await accessor.RefreshAccessToken(refreshToken);
if (openIdToken is null)
return null;
var inspection = await accessor.InspectToken(openIdToken.AccessToken);
var email = inspection.Email;
if (string.IsNullOrEmpty(email))
return null;
var user = await users.GetUserByEmail(email);
if (user is null) {
if (!openIdOptions.Value.GenerateUsers)
return null;
var username = inspection.PreferredUsername;
user = await users.AddUser(new User {
Email = email,
Username = username
});
}
accessor.SetAuthenticationCookies(openIdToken);
return new() {
Owner = user,
CreatedAt = DateTime.Now,
Type = Token.OpenIdTokenType
};
}
if (!options.Value.DefaultAuthentication)
return null;
var token = await tokens.GetToken(refreshToken);
if (token is null || token.Type != Token.RefreshTokenType) return null;
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null;
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) return null;
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
@@ -91,15 +137,12 @@ internal class AuthService(
}
public async Task<bool> IsLoggedIn() {
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
if (string.IsNullOrEmpty(accessToken)) return false;
var tokenEntry = await tokens.GetToken(accessToken);
var accessToken = context.AccessToken;
if (tokenEntry is null) return false;
if (tokenEntry.Type != Token.AccessTokenType) return false;
if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
if (tokenEntry.Owner is null) return false;
if (accessToken is null) return false;
if (accessToken.Type != Token.AccessTokenType && accessToken.Type != Token.OpenIdTokenType) return false;
if (accessToken.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false;
if (accessToken.Owner is null) return false;
return true;
}

View File

@@ -1,20 +1,23 @@
using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using HopFrame.Testing.Api.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace RestApiTest.Controllers;
namespace HopFrame.Testing.Api.Controllers;
[ApiController]
[Route("test")]
public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase {
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
[HttpGet("permissions"), Authorized]
public ActionResult<IList<Permission>> Permissions() {
return new ActionResult<IList<Permission>>(userContext.User.Permissions);
public async Task<ActionResult<IList<string>>> Permissions() {
return new ActionResult<IList<string>>(await permissions.GetFullPermissions(userContext.AccessToken));
}
[HttpGet("generate")]
@@ -50,5 +53,24 @@ public class TestController(ITokenContext userContext, DatabaseContext context)
public async Task<ActionResult<IList<Address>>> GetAddresses() {
return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
}
[HttpGet("token"), Authorized]
public async Task<ActionResult<SingleValueResult<string>>> GetApiToken() {
var token = await tokens.CreateApiToken(userContext.User, DateTime.MaxValue);
await permissions.AddPermission(token, "hopframe.admin");
await permissions.AddPermission(token, "hopframe.admin.users.read");
return LogicResult<SingleValueResult<string>>.Ok(token.TokenId.ToString());
}
[HttpDelete("token/{tokenId}")]
public async Task DeleteToken(string tokenId) {
var token = await tokens.GetToken(tokenId);
await tokens.DeleteToken(token);
}
[HttpGet("url")]
public ActionResult<string> GetUrl() {
return Ok(IOpenIdAccessor.DefaultCallback ?? "Not set");
}
}

View File

@@ -1,8 +1,8 @@
using HopFrame.Database;
using HopFrame.Testing.Api.Models;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace RestApiTest;
namespace HopFrame.Testing.Api;
public class DatabaseContext : HopDbContextBase {
@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace RestApiTest.Models;
namespace HopFrame.Testing.Api.Models;
public class Address {
[ForeignKey("Employee")]

View File

@@ -1,4 +1,4 @@
namespace RestApiTest.Models;
namespace HopFrame.Testing.Api.Models;
public class Employee {
public int EmployeeId { get; set; }

View File

@@ -1,4 +1,4 @@
using RestApiTest;
using HopFrame.Testing.Api;
using HopFrame.Api.Extensions;
using Microsoft.OpenApi.Models;
@@ -6,8 +6,9 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddHttpClient();
builder.Services.AddControllers();
builder.Services.AddHopFrame<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@@ -18,7 +19,7 @@ builder.Services.AddSwaggerGen(c => {
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
Enter 'Bearer' [space] and then your token in the text input below.",
Name = "Authorization",
Name = "Token",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"

View File

@@ -1,10 +1,10 @@
using FrontendTest.Providers;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
using RestApiTest.Models;
using HopFrame.Testing.Api.Models;
using HopFrame.Testing.Web.Providers;
namespace FrontendTest;
namespace HopFrame.Testing.Web;
public class AdminContext : AdminPagesContext {

View File

@@ -7,7 +7,7 @@
<base href="/"/>
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="app.css"/>
<link rel="stylesheet" href="FrontendTest.styles.css"/>
<link rel="stylesheet" href="HopFrame.Testing.Web.styles.css"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet/>
</head>

View File

@@ -6,5 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using FrontendTest
@using FrontendTest.Components
@using HopFrame.Testing.Web
@using HopFrame.Testing.Web.Components

View File

@@ -1,8 +1,8 @@
using HopFrame.Database;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
using HopFrame.Testing.Api.Models;
namespace FrontendTest;
namespace HopFrame.Testing.Web;
public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
@@ -11,7 +11,7 @@ public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace RestApiTest.Models;
namespace HopFrame.Testing.Api.Models;
public class Address {
[ForeignKey("Employee")]

View File

@@ -1,4 +1,4 @@
namespace RestApiTest.Models;
namespace HopFrame.Testing.Api.Models;
public class Employee {
public int EmployeeId { get; set; }

View File

@@ -1,12 +1,12 @@
using FrontendTest;
using FrontendTest.Components;
using HopFrame.Testing.Web;
using HopFrame.Testing.Web.Components;
using HopFrame.Web;
using HopFrame.Web.Admin;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
builder.Services.AddAdminContext<AdminContext>();
// Add services to the container.

View File

@@ -1,8 +1,8 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
using HopFrame.Testing.Api.Models;
namespace FrontendTest.Providers;
namespace HopFrame.Testing.Web.Providers;
public class AddressProvider(DatabaseContext context) : ModelProvider<Address> {

View File

@@ -1,8 +1,8 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
using HopFrame.Testing.Api.Models;
namespace FrontendTest.Providers;
namespace HopFrame.Testing.Web.Providers;
public class EmployeeProvider(DatabaseContext context) : ModelProvider<Employee> {

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