Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c7e38aa40 | |||
| 11126e8080 | |||
| 0b9766f7db | |||
| 849ad649a8 | |||
| 3031dda710 | |||
| 73d89a241f | |||
| df68b6dbf8 | |||
| 20684ca40a | |||
| a57127af0e | |||
| 20b82245d0 | |||
| 8db38183c2 | |||
| 5898ea8188 | |||
| 1bc48b0ba2 | |||
| 2308e1520d | |||
| 1ede337565 | |||
| a7d2f8031e | |||
| 4aab011224 | |||
| ae74745108 | |||
| 401dfc9909 | |||
| ffae1be340 | |||
| bee771a30e | |||
| 9b38a10797 | |||
| ba7584c771 | |||
| df89450745 | |||
| c6aca4baf6 | |||
| e47d4917df | |||
| 59c452ff73 | |||
| ba46147a74 | |||
| c087dbdf2b | |||
| 92afc85dba | |||
| 51c15eff4c | |||
| 422fd6c677 | |||
| 88c8fe612d | |||
| dce0471105 | |||
| c4ee8bb1e0 | |||
| 7c835ea49b | |||
| 5f746e0bc1 | |||
| ee7bf1e204 | |||
| 4d91ce1819 | |||
| a4d1d3227b | |||
| 14c82f4f06 | |||
| fca6ef4fa6 | |||
| da45a84f61 | |||
| b7eca1937c | |||
| 85031de3c2 | |||
| 1897428d00 | |||
| 4a5855250c | |||
| b206ab65ce | |||
| 92a329ef41 | |||
| 92b7e9a8ae | |||
| 776b055cfb | |||
| bd7751b44b | |||
| f8995ca990 | |||
| beac2aa20c | |||
| c00c30ea3f | |||
| e257e36b66 | |||
| fee99c60b6 | |||
| 0c2c02136d | |||
|
|
16ef41800d | ||
|
|
2bc8a5d70b | ||
|
|
a531cd7a47 | ||
| e8c61dbc7f | |||
| 01cd0e1590 | |||
| eef03c152d | |||
| 986c5cebde | |||
| 6d2f7051ee | |||
| 6c5c5c9e9d | |||
|
|
53d214ed8b | ||
| 4801e790c0 | |||
| 61323f089d | |||
|
|
94e7a41e59 | ||
| 89a3185c8b | |||
| bc7dfa8e6a | |||
| 601b502c8c | |||
| 0cc4eb44da | |||
| d38cce6dc2 | |||
| 85a45ece55 | |||
| ce15717c7d | |||
| 599ce2bf43 | |||
| d2729870e3 | |||
| bc0651cb75 | |||
| 075ca2286f | |||
| 6a781990e4 | |||
| dd67bba07d | |||
| 6a110d5b8b | |||
| 9cf818c55d | |||
| 66ddc22012 | |||
| 672f0fd2c3 | |||
|
|
8d280422cf | ||
| cfda1bd053 | |||
| f71587d72e | |||
|
|
1b3ffc82ff | ||
| c3f8615eba |
39
.gitlab-ci.yml
Normal file
39
.gitlab-ci.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
image: mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
- publish
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- echo "Setting up environment"
|
||||||
|
- 'dotnet --version'
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- dotnet restore
|
||||||
|
- dotnet build --configuration Release --no-restore
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "**/bin/Release"
|
||||||
|
expire_in: 10 minutes
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- dotnet test --verbosity normal
|
||||||
|
dependencies:
|
||||||
|
- build
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
script:
|
||||||
|
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||||
|
- dotnet pack -c Release -o . /p:Version=$VERSION
|
||||||
|
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json; done
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
dependencies:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
@@ -5,7 +5,7 @@
|
|||||||
<driver-ref>sqlite.xerial</driver-ref>
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/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>
|
<jdbc-additional-properties>
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
</jdbc-additional-properties>
|
</jdbc-additional-properties>
|
||||||
|
|||||||
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal file
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal 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>
|
||||||
1025
.idea/config/applicationhost.config
generated
Normal file
1025
.idea/config/applicationhost.config
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
HopFrame.sln
51
HopFrame.sln
@@ -2,7 +2,7 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -10,7 +10,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -42,7 +58,38 @@ Global
|
|||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
|
{003120AE-F38B-4632-8497-BE4505189627} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
|
{7F82E1C6-4A42-4337-9E03-2EE6429D004F} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
|
{3BE585BC-13A5-4BE4-A806-E9EC2D825956} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||||
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||||
|
{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||||
|
{1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||||
|
{6747753A-6059-48F1-B779-D73765A373A6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||||
|
{25DE1510-47E5-46FF-89A4-B9F99542218E} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||||
|
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,5 +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">
|
<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"><AssemblyExplorer>
|
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||||
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
||||||
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
||||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
</AssemblyExplorer></s:String>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</wpf:ResourceDictionary>
|
||||||
@@ -5,13 +5,17 @@ A simple backend management api for ASP.NET Core Web APIs
|
|||||||
- [x] Database management
|
- [x] Database management
|
||||||
- [x] User authentication
|
- [x] User authentication
|
||||||
- [x] Permission management
|
- [x] Permission management
|
||||||
- [x] Frontend dashboards
|
- [x] Generated frontend administration boards
|
||||||
|
- [x] API token support
|
||||||
|
- [x] OpenID authentication integration
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||||
|
|
||||||
## Ho to use the Web API version
|
## Ho to use the Web API version
|
||||||
|
|
||||||
|
> **Hint:** For more information about the HopFrame installation and usage go to the [docs](./docs).
|
||||||
|
|
||||||
1. Add the HopFrame.Api library to your project:
|
1. Add the HopFrame.Api library to your project:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
@startuml ApiModels
|
|
||||||
|
|
||||||
namespace HopFrame.Security {
|
|
||||||
class UserLogin {
|
|
||||||
+Email: string
|
|
||||||
+Password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserRegister {
|
|
||||||
+Username: string
|
|
||||||
+Email: string
|
|
||||||
+Password: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace HopFrame.Api {
|
|
||||||
class SingleValueResult<TValue> {
|
|
||||||
+Value: TValue
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserPasswordValidation {
|
|
||||||
+Password: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
@startuml BaseModels
|
|
||||||
set namespaceSeparator none
|
|
||||||
|
|
||||||
namespace HopFrame.Database {
|
|
||||||
class User {
|
|
||||||
+Id: Guid
|
|
||||||
+Username: string
|
|
||||||
+Email: string
|
|
||||||
+CreatedAt: DateTime
|
|
||||||
+Permissions: IList<Permission>
|
|
||||||
}
|
|
||||||
|
|
||||||
class Permission {
|
|
||||||
+Id: long
|
|
||||||
+PermissionName: string
|
|
||||||
+Owner: Guid
|
|
||||||
+GrantedAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionGroup {
|
|
||||||
+Name: string
|
|
||||||
+IsDefaultGroup: bool
|
|
||||||
+Description: string
|
|
||||||
+CreatedAt: DateTime
|
|
||||||
+Permissions: IList<Permission>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPermissionOwner {}
|
|
||||||
}
|
|
||||||
|
|
||||||
IPermissionOwner <|-- User
|
|
||||||
IPermissionOwner <|-- PermissionGroup
|
|
||||||
|
|
||||||
User .. Permission
|
|
||||||
PermissionGroup .. Permission
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
@startuml DatabaseModels
|
|
||||||
set namespaceSeparator none
|
|
||||||
|
|
||||||
namespace HopFrame.Database {
|
|
||||||
class UserEntry {
|
|
||||||
+Id: string
|
|
||||||
+Username: string
|
|
||||||
+Email: string
|
|
||||||
+Password: string
|
|
||||||
+CreatedAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
class TokenEntry {
|
|
||||||
+Type: int
|
|
||||||
+Token: string
|
|
||||||
+UserId: string
|
|
||||||
+CreatedAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionEntry {
|
|
||||||
+RecordId: long
|
|
||||||
+PermissionText: string
|
|
||||||
+UserId: string
|
|
||||||
+GrantedAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupEntry {
|
|
||||||
+Name: string
|
|
||||||
+Default: bool
|
|
||||||
+Description: string
|
|
||||||
+CreatedAt: DateTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntry *-- TokenEntry
|
|
||||||
UserEntry *-- PermissionEntry
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,13 +0,0 @@
|
|||||||
# HopFrame documentation
|
|
||||||
These sides contain all documentation available for the HopFrame modules
|
|
||||||
|
|
||||||
## Content
|
|
||||||
| Topic | Description | Document |
|
|
||||||
|----------|------------------------------------------------|-----------------------|
|
|
||||||
| Models | All models used by the HopFrame | [link](./models.md) |
|
|
||||||
| Services | All services provided by the HopFrame | [link](./services.md) |
|
|
||||||
| Usage | How to properly implement the HopFrame modules | [link](./usage.md) |
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
Both the HopFrame.Api and HopFrame.Web modules are dependent on the HopFrame.Database and HopFrame.Security modules.
|
|
||||||
So all models and services provided by these modules are available in the other modules as well.
|
|
||||||
30
docs/api/authorization.md
Normal file
30
docs/api/authorization.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# HopFrame Authentication
|
||||||
|
|
||||||
|
With the provided HopFrame services, you can secure your endpoints so that only logged-in users or users with the right permissions can access the endpoint.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
You can secure your endpoints by adding the `Authorized` attribute.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Everyone can access this endpoint
|
||||||
|
[HttpGet("hello")]
|
||||||
|
public ActionResult<string> HelloWorld() {
|
||||||
|
return "Hello, World!";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Only logged-in users can access this endpoint
|
||||||
|
[HttpGet("hello"), Authorized]
|
||||||
|
public ActionResult<string> HelloWorld() {
|
||||||
|
return "Hello, World!";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Only logged-in users with the specified permissions can access this endpoint
|
||||||
|
[HttpGet("hello"), Authorized("test.permission", "test.permission.another")]
|
||||||
|
public ActionResult<string> HelloWorld() {
|
||||||
|
return "Hello, World!";
|
||||||
|
}
|
||||||
|
```
|
||||||
21
docs/api/endpoints.md
Normal file
21
docs/api/endpoints.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# HopFrame Endpoints
|
||||||
|
HopFrame currently only supports endpoints for authentication out of the box.
|
||||||
|
|
||||||
|
> **Hint:** with the help of the [repositories](../repositories.md) you can very easily create missing endpoints for HopFrame components yourself.
|
||||||
|
|
||||||
|
## All currently supported endpoints
|
||||||
|
|
||||||
|
> **Hint:** you can use the build-in [swagger](https://swagger.io/) ui to explore and test all endpoints of your application __including__ HopFrame endpoints.
|
||||||
|
|
||||||
|
### SecurityController
|
||||||
|
Base endpoint: `/api/v1/authentication`\
|
||||||
|
**Important:** All primitive data types (including `string`) are return as a [`SingleValueResult`](./models.md#SingleValueResult)
|
||||||
|
|
||||||
|
|
||||||
|
| Method | Endpoint | Payload | Returns |
|
||||||
|
|--------|---------------|--------------------------------------------------------------|-----------------------|
|
||||||
|
| PUT | /login | [UserLogin](../models.md#UserLogin) | access token (string) |
|
||||||
|
| POST | /register | [UserRegister](../models.md#UserRegister) | access token (string) |
|
||||||
|
| GET | /authenticate | | access token (string) |
|
||||||
|
| DELETE | /logout | | |
|
||||||
|
| DELETE | /delete | [UserPasswordValidation](./models.md#UserPasswordValidation) | |
|
||||||
16
docs/api/installation.md
Normal file
16
docs/api/installation.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Ho to use the Web API version
|
||||||
|
This Installation adds all HopFrame [endpoints](./endpoints.md) and [repositories](../repositories.md) to the application.
|
||||||
|
|
||||||
|
1. Add the HopFrame.Api library to your project:
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet add package HopFrame.Api
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
|
||||||
|
|
||||||
|
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddHopFrame<DatabaseContext>();
|
||||||
|
```
|
||||||
33
docs/api/logicresults.md
Normal file
33
docs/api/logicresults.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# LogicResults
|
||||||
|
LogicResults provide another layer of abstraction above the ActionResults.
|
||||||
|
They help you sending the right `HttpStatusCode` with the right data.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Create an endpoint that returns an `ActionResult`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("hello")]
|
||||||
|
public ActionResult<string> Hello() {
|
||||||
|
return new ActionResult<string>("Hello, World!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. Now instead of directly returning the `ActionResult`, return a `LogicResult`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("hello")]
|
||||||
|
public ActionResult<string> Hello() {
|
||||||
|
return LogicResult<string>.Ok("Hello, World!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. This allows you to very easily change the return type by simply calling the right function
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("hello")]
|
||||||
|
public ActionResult<string> Hello() {
|
||||||
|
if (!Auth.IsLoggedIn)
|
||||||
|
return LogicResult<string>.Forbidden();
|
||||||
|
|
||||||
|
return LogicResult<string>.Ok("Hello, World!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> **Hint:** You can also provide an error message for status codes that are not in the 200 range.
|
||||||
16
docs/api/models.md
Normal file
16
docs/api/models.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# HopFrame Models
|
||||||
|
All models used by the RestAPI are listed below
|
||||||
|
|
||||||
|
## SingleValueResult
|
||||||
|
```csharp
|
||||||
|
public struct SingleValueResult<TValue>(TValue value) {
|
||||||
|
public TValue Value { get; set; } = value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UserPasswordValidation
|
||||||
|
```csharp
|
||||||
|
public sealed class UserPasswordValidation {
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
76
docs/authentication.md
Normal file
76
docs/authentication.md
Normal 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");
|
||||||
|
```
|
||||||
144
docs/blazor/admin.md
Normal file
144
docs/blazor/admin.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# HopFrame Admin Pages
|
||||||
|
Admin pages can be defined through a `AdminContext` similar to how a `DbContext` is defined. They generate administration pages like [`/administration/users`](./pages.md)
|
||||||
|
simply by reading the structure of the provided model and optionally some additional configuration.
|
||||||
|
|
||||||
|
> **Fun fact:** The already existing pages `/administration/users` and `/administration/groups` are also generated using an internal `AdminContext`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Create a class that inherits the `AdminPagesContext` base class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AdminContext : AdminPagesContext {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add your admin pages as properties to the class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AdminContext : AdminPagesContext {
|
||||||
|
|
||||||
|
public AdminPage<Address> Addresses { get; set; }
|
||||||
|
|
||||||
|
public AdminPage<Employee> Employees { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Hint:** you can specify the url of the admin page by adding the `AdminPageUrl` Attribute
|
||||||
|
|
||||||
|
3. **Optionally** you can further configure your pages in the `OnModelCreating` method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AdminContext : AdminPagesContext {
|
||||||
|
|
||||||
|
public AdminPage<Address> Addresses { get; set; }
|
||||||
|
public AdminPage<Employee> Employees { get; set; }
|
||||||
|
|
||||||
|
public override void OnModelCreating(IAdminContextGenerator generator) {
|
||||||
|
base.OnModelCreating(generator);
|
||||||
|
|
||||||
|
generator.Page<Employee>()
|
||||||
|
.Property(e => e.Address)
|
||||||
|
.IsSelector();
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.Property(a => a.Employee)
|
||||||
|
.Ignore();
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.Property(a => a.AddressId)
|
||||||
|
.IsSelector<Employee>()
|
||||||
|
.Parser<Employee>((model, e) => model.AddressId = e.EmployeeId);
|
||||||
|
|
||||||
|
generator.Page<Employee>()
|
||||||
|
.ConfigureRepository<EmployeeProvider>()
|
||||||
|
.ListingProperty(e => e.Name);
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.ConfigureRepository<AddressProvider>()
|
||||||
|
.ListingProperty(a => a.City);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\
|
||||||
|
\
|
||||||
|
Attributes for classes and properties:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||||
|
public sealed class AdminNameAttribute(string name) : Attribute {
|
||||||
|
public string Name { get; set; } = name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Attributes for classes:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
|
||||||
|
public bool ShowCreateButton { get; set; } = showCreateButton;
|
||||||
|
public bool ShowDeleteButton { get; set; } = showDeleteButton;
|
||||||
|
public bool ShowUpdateButton { get; set; } = showUpdateButton;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
|
||||||
|
public AdminPagePermissions Permissions { get; set; } = new() {
|
||||||
|
Create = create,
|
||||||
|
Update = update,
|
||||||
|
Delete = delete,
|
||||||
|
View = view
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminDescriptionAttribute(string description) : Attribute {
|
||||||
|
public string Description { get; set; } = description;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Attributes for properties:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminHideValueAttribute : Attribute;
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
|
||||||
|
public bool OnlyForListing { get; set; } = onlyForListing;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
|
||||||
|
public string Prefix { get; set; } = prefix;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminUneditableAttribute : Attribute;
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class AdminUniqueAttribute : Attribute;
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminUnsortableAttribute : Attribute;
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class ListingPropertyAttribute : Attribute;
|
||||||
|
```
|
||||||
20
docs/blazor/auth.md
Normal file
20
docs/blazor/auth.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Auth Service
|
||||||
|
The `IAuthService` provides some useful methods to handle user authentication (login/register).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Simply define the `IAuthService` as a dependency
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IAuthService {
|
||||||
|
Task Register(UserRegister register);
|
||||||
|
Task<bool> Login(UserLogin login);
|
||||||
|
Task Logout();
|
||||||
|
|
||||||
|
Task<Token> RefreshLogin();
|
||||||
|
Task<bool> IsLoggedIn();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Automatically refresh user sessions
|
||||||
|
1. Make sure you have implemented the `AuthMiddleware` how it's described in step 5 of the [installation](./installation.md).
|
||||||
|
|
||||||
|
2. After that, the access token of the user gets automatically refreshed as long as the refresh token is valid.
|
||||||
20
docs/blazor/authorization.md
Normal file
20
docs/blazor/authorization.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# HopFrame Authentication
|
||||||
|
|
||||||
|
With the provided HopFrame services, you can secure your blazor pages so that only logged-in users or users with the right permissions can access the page.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
You can secure your Blazor pages by using the `AuthorizedView` component.
|
||||||
|
Everything placed inside this component will only be displayed if the authorization was successful.
|
||||||
|
You can also redirect the user if the authorization fails by specifying a `RedirectIfUnauthorized` url.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- You can either specify one 'Permission', multiple 'Permissions' or none if the user only needs to be logged-in -->
|
||||||
|
<AuthorizedView Permission="test.permission">
|
||||||
|
<p>This paragraph is only visible if the user is logged-in and has the required permission</p>
|
||||||
|
</AuthorizedView>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- This component will redirect the user to the login page if the user is unauthorized -->
|
||||||
|
<AuthorizedView RedirectIfUnauthorized="/login" />
|
||||||
|
```
|
||||||
36
docs/blazor/installation.md
Normal file
36
docs/blazor/installation.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
## How to use the Blazor API
|
||||||
|
This Installation adds all HopFrame [pages](./pages.md) and [repositories](../repositories.md) to the application.
|
||||||
|
|
||||||
|
1. Add the HopFrame.Web library to your project
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet add package HopFrame.Web
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
|
||||||
|
|
||||||
|
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddHopFrame<DatabaseContext>();
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Optional:** You can also add your [AdminContext](./admin.md)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddAdminContext<AdminContext>();
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add the authentication middleware to your app
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.UseMiddleware<AuthMiddleware>();
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Add the HopFrame pages to your Razor components
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddHopFrameAdminPages()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
```
|
||||||
14
docs/blazor/pages.md
Normal file
14
docs/blazor/pages.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# HopFrame Pages
|
||||||
|
By default, the HopFrame provides some blazor pages for managing user accounts and permissions
|
||||||
|
|
||||||
|
## All currently supported blazor pages
|
||||||
|
|
||||||
|
| Page | Endpoint | Permission | Usage |
|
||||||
|
|-----------------|------------------------|----------------------------|--------------------------------------------------------------------------------------------------------|
|
||||||
|
| Admin Dashboard | /administration | hopframe.admin | This page provides an overview to all admin pages built-in and created by [AdminContexts](./admin.md). |
|
||||||
|
| Admin Login | /administration/login | | This page is a simple login screen so no login screen needs to be created to access the admin pages. |
|
||||||
|
| User Dashboard | /administration/users | hopframe.admin.users.view | This page serves as a management site for all users and their permissions. |
|
||||||
|
| Group Dashboard | /administration/groups | hopframe.admin.groups.view | This page serves as a management site for all groups and their permissions. |
|
||||||
|
|
||||||
|
> **Hint:** All pages created by [AdminContexts](./admin.md) are also under the `/administration/` location. This can unfortunately __not__ be changed at the moment.
|
||||||
|
|
||||||
35
docs/database.md
Normal file
35
docs/database.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Database initialization
|
||||||
|
You also need to initialize the data source with the tables from HopFrame.
|
||||||
|
|
||||||
|
## Create a DbContext
|
||||||
|
|
||||||
|
1. Create a c# class that inherits from the `HopDbContextBase` and add a data source (In the example Sqlite is used)\
|
||||||
|
**IMPORTANT:** You need to leave the `base.OnConfiguring(optionsBuilder)` in place so the HopFrame model relations are set correctly.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DatabaseContext : HopDbContextBase {
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||||
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
|
||||||
|
optionsBuilder.UseSqlite("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register the `DatabaseContext` as a service
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddDbContext<DatabaseContext>();
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a database migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet ef migrations add Initial
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Apply the migration to the data source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
@@ -1,21 +1,73 @@
|
|||||||
# Models for HopFrame
|
# HopFrame base models
|
||||||
|
All models listed below are part of the core HopFrame components and accessible in all installation variations
|
||||||
|
|
||||||
This page shows all models that HopFrame uses.
|
> **Note:** All properties of the models that are `virtual` are relational properties and don't directly correspond to columns in the database.
|
||||||
|
|
||||||
|
## User
|
||||||
|
```csharp
|
||||||
|
public class User : IPermissionOwner {
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
public virtual List<Token> Tokens { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Base Models
|
## PermissionGroup
|
||||||
These are the models used by the various database services.
|
```csharp
|
||||||
|
public class PermissionGroup : IPermissionOwner {
|
||||||
|
public string Name { get; init; }
|
||||||
|
public bool IsDefaultGroup { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||

|
## Permission
|
||||||
|
```csharp
|
||||||
|
public class Permission {
|
||||||
|
public long Id { get; init; }
|
||||||
|
public string PermissionName { get; set; }
|
||||||
|
public DateTime GrantedAt { get; set; }
|
||||||
|
public virtual User User { get; set; }
|
||||||
|
public virtual PermissionGroup Group { get; set; }
|
||||||
|
public virtual Token Token { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token
|
||||||
|
```csharp
|
||||||
|
public class Token : IPermissionOwner {
|
||||||
|
public int Type { get; set; }
|
||||||
|
public Guid Content { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public virtual User Owner { get; set; }
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## API Models
|
## UserLogin
|
||||||
These are the models used by the REST API and the Blazor API.
|
```csharp
|
||||||
|
public class UserLogin {
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||

|
## UserRegister
|
||||||
|
```csharp
|
||||||
|
public class UserRegister {
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## IPermissionOwner
|
||||||
## Database Models
|
```csharp
|
||||||
These are the models that correspond to the scheme in the Database
|
public interface IPermissionOwner;
|
||||||
|
```
|
||||||

|
|
||||||
|
|||||||
120
docs/openid.md
Normal file
120
docs/openid.md
Normal 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
80
docs/permissions.md
Normal 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"
|
||||||
|
```
|
||||||
28
docs/readme.md
Normal file
28
docs/readme.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# HopFrame Documentation
|
||||||
|
|
||||||
|
The HopFrame comes in two variations, you can eiter only use the backend with some basic endpoints or with fully fledged blazor pages for managing HopFrame components.
|
||||||
|
|
||||||
|
## Shared HopFrame Modules
|
||||||
|
|
||||||
|
- [Database](./database.md)
|
||||||
|
- [Repositories](./repositories.md)
|
||||||
|
- [Base Models](./models.md)
|
||||||
|
- [Authentication](./authentication.md)
|
||||||
|
- [Permissions](./permissions.md)
|
||||||
|
- [OpenID Integration](./openid.md)
|
||||||
|
|
||||||
|
## HopFrame Web API
|
||||||
|
|
||||||
|
- [Installation](./api/installation.md)
|
||||||
|
- [Endpoints](./api/endpoints.md)
|
||||||
|
- [Authorization](./api/authorization.md)
|
||||||
|
- [Models](./api/models.md)
|
||||||
|
- [LogicResults](./api/logicresults.md)
|
||||||
|
|
||||||
|
## HopFrame Blazor library
|
||||||
|
|
||||||
|
- [Installation](./blazor/installation.md)
|
||||||
|
- [Pages](./blazor/pages.md)
|
||||||
|
- [Authorization](./blazor/authorization.md)
|
||||||
|
- [Auth Service](./blazor/auth.md)
|
||||||
|
- [Admin Context](./blazor/admin.md)
|
||||||
79
docs/repositories.md
Normal file
79
docs/repositories.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# HopFrame Repositories
|
||||||
|
The HopFrame provies repositories for the various build in database models as an abstraction around the `HopDbContext` to ensure, that the data is proccessed and saved correctly.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The repositories can also be used by simply defining them as a dependency in your service / controller.
|
||||||
|
|
||||||
|
### User Repository
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IUserRepository {
|
||||||
|
Task<IList<User>> GetUsers();
|
||||||
|
|
||||||
|
Task<User> GetUser(Guid userId);
|
||||||
|
|
||||||
|
Task<User> GetUserByEmail(string email);
|
||||||
|
|
||||||
|
Task<User> GetUserByUsername(string username);
|
||||||
|
|
||||||
|
Task<User> AddUser(User user);
|
||||||
|
|
||||||
|
Task UpdateUser(User user);
|
||||||
|
|
||||||
|
Task DeleteUser(User user);
|
||||||
|
|
||||||
|
Task<bool> CheckUserPassword(User user, string password);
|
||||||
|
|
||||||
|
Task ChangePassword(User user, string password);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group Repository
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IGroupRepository {
|
||||||
|
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||||
|
|
||||||
|
Task<IList<PermissionGroup>> GetDefaultGroups();
|
||||||
|
|
||||||
|
Task<IList<PermissionGroup>> GetUserGroups(User user);
|
||||||
|
|
||||||
|
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||||
|
|
||||||
|
Task EditPermissionGroup(PermissionGroup group);
|
||||||
|
|
||||||
|
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
|
||||||
|
|
||||||
|
Task DeletePermissionGroup(PermissionGroup group);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Repository
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPermissionRepository {
|
||||||
|
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
|
||||||
|
|
||||||
|
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
|
||||||
|
|
||||||
|
Task RemovePermission(IPermissionOwner owner, string permission);
|
||||||
|
|
||||||
|
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Repository
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ITokenRepository {
|
||||||
|
Task<Token> GetToken(string content);
|
||||||
|
|
||||||
|
Task<Token> CreateToken(int type, User owner);
|
||||||
|
|
||||||
|
Task DeleteUserTokens(User owner);
|
||||||
|
|
||||||
|
Task DeleteToken(Token token);
|
||||||
|
|
||||||
|
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||||
|
}
|
||||||
|
```
|
||||||
145
docs/services.md
145
docs/services.md
@@ -1,145 +0,0 @@
|
|||||||
# HopFrame Services
|
|
||||||
This page describes all services provided by the HopFrame.
|
|
||||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
|
||||||
|
|
||||||
## HopFrame.Security
|
|
||||||
### ITokenContext
|
|
||||||
This service provides the information given by the current request
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface ITokenContext {
|
|
||||||
bool IsAuthenticated { get; }
|
|
||||||
|
|
||||||
User User { get; }
|
|
||||||
|
|
||||||
Guid AccessToken { get; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IUserService
|
|
||||||
This service simplifies the data access of the user table in the database.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IUserService {
|
|
||||||
Task<IList<User>> GetUsers();
|
|
||||||
|
|
||||||
Task<User> GetUser(Guid userId);
|
|
||||||
|
|
||||||
Task<User> GetUserByEmail(string email);
|
|
||||||
|
|
||||||
Task<User> GetUserByUsername(string username);
|
|
||||||
|
|
||||||
Task<User> AddUser(UserRegister user);
|
|
||||||
|
|
||||||
Task UpdateUser(User user);
|
|
||||||
|
|
||||||
Task DeleteUser(User user);
|
|
||||||
|
|
||||||
Task<bool> CheckUserPassword(User user, string password);
|
|
||||||
|
|
||||||
Task ChangePassword(User user, string password);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IPermissionService
|
|
||||||
This service handles all permission and group interactions with the data source.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IPermissionService {
|
|
||||||
Task<bool> HasPermission(string permission, Guid user);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
|
||||||
|
|
||||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
|
||||||
|
|
||||||
Task EditPermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
|
||||||
|
|
||||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
|
||||||
|
|
||||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
|
||||||
|
|
||||||
Task DeletePermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
|
||||||
|
|
||||||
Task AddPermission(IPermissionOwner owner, string permission);
|
|
||||||
|
|
||||||
Task RemovePermission(Permission permission);
|
|
||||||
|
|
||||||
Task<string[]> GetFullPermissions(string user);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## HopFrame.Api
|
|
||||||
### LogicResult
|
|
||||||
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class LogicResult : ILogicResult {
|
|
||||||
public static LogicResult Ok();
|
|
||||||
|
|
||||||
public static LogicResult BadRequest();
|
|
||||||
|
|
||||||
public static LogicResult BadRequest(string message);
|
|
||||||
|
|
||||||
public static LogicResult Forbidden();
|
|
||||||
|
|
||||||
public static LogicResult Forbidden(string message);
|
|
||||||
|
|
||||||
public static LogicResult NotFound();
|
|
||||||
|
|
||||||
public static LogicResult NotFound(string message);
|
|
||||||
|
|
||||||
public static LogicResult Conflict();
|
|
||||||
|
|
||||||
public static LogicResult Conflict(string message);
|
|
||||||
|
|
||||||
public static LogicResult Forward(LogicResult result);
|
|
||||||
|
|
||||||
public static LogicResult Forward<T>(ILogicResult<T> result);
|
|
||||||
|
|
||||||
public static implicit operator ActionResult(LogicResult v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogicResult<T> : ILogicResult<T> {
|
|
||||||
public static LogicResult<T> Ok();
|
|
||||||
|
|
||||||
public static LogicResult<T> Ok(T result);
|
|
||||||
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IAuthLogic
|
|
||||||
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IAuthLogic {
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
|
|
||||||
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
|
||||||
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
|
||||||
|
|
||||||
Task<LogicResult> Logout();
|
|
||||||
|
|
||||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## HopFrame.Web
|
|
||||||
### IAuthService
|
|
||||||
This service handles all the authentication like login or register. It properly creates all tokens so the user can be identified
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IAuthService {
|
|
||||||
Task Register(UserRegister register);
|
|
||||||
Task<bool> Login(UserLogin login);
|
|
||||||
Task Logout();
|
|
||||||
|
|
||||||
Task<TokenEntry> RefreshLogin();
|
|
||||||
Task<bool> IsLoggedIn();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# HopFrame Usage
|
|
||||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
|
||||||
|
|
||||||
## Ho to use the Web API version
|
|
||||||
|
|
||||||
1. Add the HopFrame.Api library to your project:
|
|
||||||
|
|
||||||
```
|
|
||||||
dotnet add package HopFrame.Api
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class DatabaseContext : HopDbContextBase {
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
|
|
||||||
optionsBuilder.UseSqlite("...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Add the DbContext and HopFrame to your services
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>();
|
|
||||||
builder.Services.AddHopFrame<DatabaseContext>();
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to use the Blazor API
|
|
||||||
|
|
||||||
1. Add the HopFrame.Web library to your project
|
|
||||||
|
|
||||||
```
|
|
||||||
dotnet add package HopFrame.Web
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class DatabaseContext : HopDbContextBase {
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
|
|
||||||
optionsBuilder.UseSqlite("...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Add the DbContext and HopFrame to your services
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>();
|
|
||||||
builder.Services.AddHopFrame<DatabaseContext>();
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Add the authentication middleware to your app
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
app.UseMiddleware<AuthMiddleware>();
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Add the HopFrame pages to your Razor components
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
app.MapRazorComponents<App>()
|
|
||||||
.AddHopFrameAdminPages()
|
|
||||||
.AddInteractiveServerRenderMode();
|
|
||||||
```
|
|
||||||
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace HopFrame.Api.Controller;
|
namespace HopFrame.Api.Controller;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("authentication")]
|
[Route("api/v1/auth")]
|
||||||
public class SecurityController(IAuthLogic auth) : ControllerBase {
|
public class AuthController(IAuthLogic auth) : ControllerBase {
|
||||||
|
|
||||||
[HttpPut("login")]
|
[HttpPut("login")]
|
||||||
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
||||||
74
src/HopFrame.Api/Controller/GroupController.cs
Normal file
74
src/HopFrame.Api/Controller/GroupController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
src/HopFrame.Api/Controller/OpenIdController.cs
Normal file
67
src/HopFrame.Api/Controller/OpenIdController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/HopFrame.Api/Controller/UserController.cs
Normal file
83
src/HopFrame.Api/Controller/UserController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ using HopFrame.Api.Logic;
|
|||||||
using HopFrame.Api.Logic.Implementation;
|
using HopFrame.Api.Logic.Implementation;
|
||||||
using HopFrame.Database;
|
using HopFrame.Database;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
@@ -15,22 +17,43 @@ public static class ServiceCollectionExtensions {
|
|||||||
/// Adds all HopFrame endpoints and services to the application
|
/// Adds all HopFrame endpoints and services to the application
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service provider to add the services to</param>
|
/// <param name="services">The service provider to add the services to</param>
|
||||||
|
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||||
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
|
var controllers = new List<Type> { typeof(UserController), typeof(GroupController) };
|
||||||
AddHopFrameNoEndpoints<TDbContext>(services);
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Adds all HopFrame services to the application
|
/// Adds all HopFrame services to the application
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service provider to add the services to</param>
|
/// <param name="services">The service provider to add the services to</param>
|
||||||
|
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) 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.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
services.AddScoped<IAuthLogic, AuthLogic<TDbContext>>();
|
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||||
|
services.AddScoped<IUserLogic, UserLogic>();
|
||||||
|
services.AddScoped<IGroupLogic, GroupLogic>();
|
||||||
|
|
||||||
services.AddHopFrameAuthentication<TDbContext>();
|
services.AddHopFrameAuthentication(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
|
|
||||||
<PackageId>HopFrame.Api</PackageId>
|
<PackageId>HopFrame.Api</PackageId>
|
||||||
<Version>1.1.0</Version>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
@@ -22,4 +21,10 @@
|
|||||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>HopFrame.Tests.Api</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,9 +8,18 @@ public interface IAuthLogic {
|
|||||||
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reassures that the user has a valid refresh token and generates a new access token
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The newly generated access token</returns>
|
||||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
||||||
|
|
||||||
Task<LogicResult> Logout();
|
Task<LogicResult> Logout();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the user account that called the endpoint if the provided password is correct
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="validation">The password od the user</param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
Task<LogicResult> Delete(UserPasswordValidation validation);
|
||||||
}
|
}
|
||||||
14
src/HopFrame.Api/Logic/IGroupLogic.cs
Normal file
14
src/HopFrame.Api/Logic/IGroupLogic.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace HopFrame.Api.Logic;
|
namespace HopFrame.Api.Logic;
|
||||||
|
|
||||||
|
|||||||
16
src/HopFrame.Api/Logic/IUserLogic.cs
Normal file
16
src/HopFrame.Api/Logic/IUserLogic.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
using HopFrame.Api.Models;
|
using HopFrame.Api.Models;
|
||||||
using HopFrame.Database;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Models.Entries;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Models;
|
using HopFrame.Security.Models;
|
||||||
using HopFrame.Security.Services;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace HopFrame.Api.Logic.Implementation;
|
namespace HopFrame.Api.Logic.Implementation;
|
||||||
|
|
||||||
public class AuthLogic<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic where TDbContext : HopDbContextBase {
|
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) {
|
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||||
|
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||||
|
|
||||||
var user = await users.GetUserByEmail(login.Email);
|
var user = await users.GetUserByEmail(login.Email);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
@@ -21,107 +22,84 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
|||||||
if (!await users.CheckUserPassword(user, login.Password))
|
if (!await users.CheckUserPassword(user, login.Password))
|
||||||
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
|
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
|
||||||
|
|
||||||
var refreshToken = new TokenEntry {
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
CreatedAt = DateTime.Now,
|
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||||
Token = Guid.NewGuid().ToString(),
|
|
||||||
Type = TokenEntry.RefreshTokenType,
|
|
||||||
UserId = user.Id.ToString()
|
|
||||||
};
|
|
||||||
var accessToken = new TokenEntry {
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
Token = Guid.NewGuid().ToString(),
|
|
||||||
Type = TokenEntry.AccessTokenType,
|
|
||||||
UserId = user.Id.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
|
MaxAge = options.Value.RefreshTokenTime,
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||||
|
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||||
|
|
||||||
if (register.Password.Length < 8)
|
if (register.Password.Length < 8)
|
||||||
return LogicResult<SingleValueResult<string>>.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();
|
var allUsers = await users.GetUsers();
|
||||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
|
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
|
||||||
|
|
||||||
var user = await users.AddUser(register);
|
var user = await users.AddUser(new User {
|
||||||
|
Username = register.Username,
|
||||||
|
Email = register.Email,
|
||||||
|
Password = register.Password
|
||||||
|
});
|
||||||
|
|
||||||
var refreshToken = new TokenEntry {
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
CreatedAt = DateTime.Now,
|
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||||
Token = Guid.NewGuid().ToString(),
|
|
||||||
Type = TokenEntry.RefreshTokenType,
|
|
||||||
UserId = user.Id.ToString()
|
|
||||||
};
|
|
||||||
var accessToken = new TokenEntry {
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
Token = Guid.NewGuid().ToString(),
|
|
||||||
Type = TokenEntry.AccessTokenType,
|
|
||||||
UserId = user.Id.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||||
await context.SaveChangesAsync();
|
MaxAge = options.Value.RefreshTokenTime,
|
||||||
|
|
||||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
|
||||||
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
|
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||||
|
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||||
|
|
||||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(refreshToken))
|
if (string.IsNullOrEmpty(refreshToken))
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided");
|
||||||
|
|
||||||
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
|
var token = await tokens.GetToken(refreshToken);
|
||||||
|
|
||||||
if (token is null)
|
if (token is null)
|
||||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||||
|
|
||||||
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
|
if (token.Type != Token.RefreshTokenType)
|
||||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token");
|
||||||
|
|
||||||
var accessToken = new TokenEntry {
|
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now)
|
||||||
CreatedAt = DateTime.Now,
|
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
|
||||||
Token = Guid.NewGuid().ToString(),
|
|
||||||
Type = TokenEntry.AccessTokenType,
|
|
||||||
UserId = token.UserId
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.Tokens.AddAsync(accessToken);
|
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult> Logout() {
|
public async Task<LogicResult> Logout() {
|
||||||
@@ -129,19 +107,7 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
|||||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
||||||
return LogicResult.Conflict("access or refresh token not provided");
|
await tokens.DeleteUserTokens(tokenContext.User);
|
||||||
|
|
||||||
var tokenEntries = await context.Tokens.Where(token =>
|
|
||||||
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
|
|
||||||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
if (tokenEntries.Length != 2)
|
|
||||||
return LogicResult.NotFound("One or more of the provided tokens was not found");
|
|
||||||
|
|
||||||
context.Tokens.Remove(tokenEntries[0]);
|
|
||||||
context.Tokens.Remove(tokenEntries[1]);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||||
|
|||||||
66
src/HopFrame.Api/Logic/Implementation/GroupLogic.cs
Normal file
66
src/HopFrame.Api/Logic/Implementation/GroupLogic.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/HopFrame.Api/Logic/Implementation/UserLogic.cs
Normal file
105
src/HopFrame.Api/Logic/Implementation/UserLogic.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
namespace HopFrame.Api.Models;
|
namespace HopFrame.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Useful for endpoints that only return a single int or string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the result</param>
|
||||||
|
/// <typeparam name="TValue">The type of the result</typeparam>
|
||||||
public struct SingleValueResult<TValue>(TValue value) {
|
public struct SingleValueResult<TValue>(TValue value) {
|
||||||
public TValue Value { get; set; } = value;
|
public TValue Value { get; set; } = value;
|
||||||
|
|
||||||
|
|||||||
8
src/HopFrame.Api/Models/UserCreator.cs
Normal file
8
src/HopFrame.Api/Models/UserCreator.cs
Normal 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; }
|
||||||
|
}
|
||||||
6
src/HopFrame.Api/Models/UserPasswordChange.cs
Normal file
6
src/HopFrame.Api/Models/UserPasswordChange.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Api.Models;
|
||||||
|
|
||||||
|
public class UserPasswordChange {
|
||||||
|
public string OldPassword { get; set; }
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
}
|
||||||
@@ -1,100 +1,4 @@
|
|||||||
# HopFrame API module
|
# HopFrame API module
|
||||||
This module contains some useful endpoints for user login / register management.
|
This module contains some useful endpoints for user login / register management.
|
||||||
|
|
||||||
## Ho to use the Web API version
|
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||||
|
|
||||||
1. Add the HopFrame.Api library to your project:
|
|
||||||
|
|
||||||
```
|
|
||||||
dotnet add package HopFrame.Api
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class DatabaseContext : HopDbContextBase {
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
|
|
||||||
optionsBuilder.UseSqlite("...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Add the DbContext and HopFrame to your services
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>();
|
|
||||||
builder.Services.AddHopFrame<DatabaseContext>();
|
|
||||||
```
|
|
||||||
|
|
||||||
# Endpoints
|
|
||||||
By default, the module provides a controller for handling authentication based requests by the user.
|
|
||||||
You can explore the contoller by the build in swagger site from ASP .NET.
|
|
||||||
|
|
||||||
## Disable the Endpoints
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>();
|
|
||||||
//builder.Services.AddHopFrame<DatabaseContext>();
|
|
||||||
services.AddHopFrameNoEndpoints<TDbContext>();
|
|
||||||
```
|
|
||||||
|
|
||||||
# Services added in this module
|
|
||||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
|
||||||
|
|
||||||
## LogicResult
|
|
||||||
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class LogicResult : ILogicResult {
|
|
||||||
public static LogicResult Ok();
|
|
||||||
|
|
||||||
public static LogicResult BadRequest();
|
|
||||||
|
|
||||||
public static LogicResult BadRequest(string message);
|
|
||||||
|
|
||||||
public static LogicResult Forbidden();
|
|
||||||
|
|
||||||
public static LogicResult Forbidden(string message);
|
|
||||||
|
|
||||||
public static LogicResult NotFound();
|
|
||||||
|
|
||||||
public static LogicResult NotFound(string message);
|
|
||||||
|
|
||||||
public static LogicResult Conflict();
|
|
||||||
|
|
||||||
public static LogicResult Conflict(string message);
|
|
||||||
|
|
||||||
public static LogicResult Forward(LogicResult result);
|
|
||||||
|
|
||||||
public static LogicResult Forward<T>(ILogicResult<T> result);
|
|
||||||
|
|
||||||
public static implicit operator ActionResult(LogicResult v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogicResult<T> : ILogicResult<T> {
|
|
||||||
public static LogicResult<T> Ok();
|
|
||||||
|
|
||||||
public static LogicResult<T> Ok(T result);
|
|
||||||
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## IAuthLogic
|
|
||||||
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IAuthLogic {
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
|
|
||||||
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
|
||||||
|
|
||||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
|
||||||
|
|
||||||
Task<LogicResult> Logout();
|
|
||||||
|
|
||||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
|
|
||||||
namespace HopFrame.Security;
|
namespace HopFrame.Database;
|
||||||
|
|
||||||
public static class EncryptionManager {
|
public static class EncryptionManager {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using HopFrame.Database.Models.Entries;
|
using HopFrame.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace HopFrame.Database;
|
namespace HopFrame.Database;
|
||||||
@@ -8,25 +8,32 @@ namespace HopFrame.Database;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class HopDbContextBase : DbContext {
|
public abstract class HopDbContextBase : DbContext {
|
||||||
|
|
||||||
public virtual DbSet<UserEntry> Users { get; set; }
|
public virtual DbSet<User> Users { get; set; }
|
||||||
public virtual DbSet<PermissionEntry> Permissions { get; set; }
|
public virtual DbSet<Permission> Permissions { get; set; }
|
||||||
public virtual DbSet<TokenEntry> Tokens { get; set; }
|
public virtual DbSet<Token> Tokens { get; set; }
|
||||||
public virtual DbSet<GroupEntry> Groups { get; set; }
|
public virtual DbSet<PermissionGroup> Groups { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity<UserEntry>();
|
modelBuilder.Entity<User>()
|
||||||
modelBuilder.Entity<PermissionEntry>();
|
.HasMany(u => u.Tokens)
|
||||||
modelBuilder.Entity<TokenEntry>();
|
.WithOne(t => t.Owner)
|
||||||
modelBuilder.Entity<GroupEntry>();
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
modelBuilder.Entity<User>()
|
||||||
/// Gets executed when a user is deleted through the IUserService from the
|
.HasMany(u => u.Permissions)
|
||||||
/// HopFrame.Security package. You can override this method to also delete
|
.WithOne(p => p.User)
|
||||||
/// related user specific entries in the database
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
/// </summary>
|
|
||||||
/// <param name="user"></param>
|
modelBuilder.Entity<PermissionGroup>()
|
||||||
public virtual void OnUserDelete(UserEntry user) {}
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
|
|
||||||
<PackageId>HopFrame.Database</PackageId>
|
<PackageId>HopFrame.Database</PackageId>
|
||||||
<Version>1.1.0</Version>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -21,4 +21,10 @@
|
|||||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>HopFrame.Tests.Database</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace HopFrame.Database.Models.Entries;
|
|
||||||
|
|
||||||
public class GroupEntry {
|
|
||||||
[Key, Required, MaxLength(50)]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[Required, DefaultValue(false)]
|
|
||||||
public bool Default { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(500)]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace HopFrame.Database.Models.Entries;
|
|
||||||
|
|
||||||
public sealed class PermissionEntry {
|
|
||||||
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public long RecordId { get; set; }
|
|
||||||
|
|
||||||
[Required, MaxLength(255)]
|
|
||||||
public string PermissionText { get; set; }
|
|
||||||
|
|
||||||
[Required, MinLength(36), MaxLength(36)]
|
|
||||||
public string UserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public DateTime GrantedAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace HopFrame.Database.Models.Entries;
|
|
||||||
|
|
||||||
public class TokenEntry {
|
|
||||||
public const int RefreshTokenType = 0;
|
|
||||||
public const int AccessTokenType = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the Type of the stored Token
|
|
||||||
/// 0: Refresh token
|
|
||||||
/// 1: Access token
|
|
||||||
/// </summary>
|
|
||||||
[Required, MinLength(1), MaxLength(1)]
|
|
||||||
public int Type { get; set; }
|
|
||||||
|
|
||||||
[Key, Required, MinLength(36), MaxLength(36)]
|
|
||||||
public string Token { get; set; }
|
|
||||||
|
|
||||||
[Required, MinLength(36), MaxLength(36)]
|
|
||||||
public string UserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace HopFrame.Database.Models.Entries;
|
|
||||||
|
|
||||||
public class UserEntry {
|
|
||||||
[Key, Required, MinLength(36), MaxLength(36)]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(50)]
|
|
||||||
public string Username { get; set; }
|
|
||||||
|
|
||||||
[Required, MaxLength(50), EmailAddress]
|
|
||||||
public string Email { get; set; }
|
|
||||||
|
|
||||||
[Required, MinLength(8), MaxLength(255)]
|
|
||||||
public string Password { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using HopFrame.Database.Models.Entries;
|
|
||||||
|
|
||||||
namespace HopFrame.Database.Models;
|
|
||||||
|
|
||||||
public static class ModelExtensions {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the database model to a friendly user model
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entry">the database model</param>
|
|
||||||
/// <param name="contextBase">the data source for the permissions and users</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) {
|
|
||||||
var user = new User {
|
|
||||||
Id = Guid.Parse(entry.Id),
|
|
||||||
Username = entry.Username,
|
|
||||||
Email = entry.Email,
|
|
||||||
CreatedAt = entry.CreatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
user.Permissions = contextBase.Permissions
|
|
||||||
.Where(perm => perm.UserId == entry.Id)
|
|
||||||
.Select(perm => perm.ToPermissionModel())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Permission ToPermissionModel(this PermissionEntry entry) {
|
|
||||||
Guid.TryParse(entry.UserId, out var userId);
|
|
||||||
|
|
||||||
return new Permission {
|
|
||||||
Owner = userId,
|
|
||||||
PermissionName = entry.PermissionText,
|
|
||||||
GrantedAt = entry.GrantedAt,
|
|
||||||
Id = entry.RecordId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PermissionGroup ToPermissionGroup(this GroupEntry entry, HopDbContextBase contextBase) {
|
|
||||||
var group = new PermissionGroup {
|
|
||||||
Name = entry.Name,
|
|
||||||
IsDefaultGroup = entry.Default,
|
|
||||||
Description = entry.Description,
|
|
||||||
CreatedAt = entry.CreatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
group.Permissions = contextBase.Permissions
|
|
||||||
.Where(perm => perm.UserId == group.Name)
|
|
||||||
.Select(perm => perm.ToPermissionModel())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,29 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace HopFrame.Database.Models;
|
namespace HopFrame.Database.Models;
|
||||||
|
|
||||||
public sealed class Permission {
|
public class Permission {
|
||||||
|
|
||||||
|
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public long Id { get; init; }
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
public string PermissionName { get; set; }
|
public string PermissionName { get; set; }
|
||||||
public Guid Owner { get; set; }
|
|
||||||
|
[Required]
|
||||||
public DateTime GrantedAt { get; set; }
|
public DateTime GrantedAt { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("UserId"), JsonIgnore]
|
||||||
|
public virtual User User { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("GroupName"), JsonIgnore]
|
||||||
|
public virtual PermissionGroup Group { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("TokenId"), JsonIgnore]
|
||||||
|
public virtual Token Token { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPermissionOwner {}
|
public interface IPermissionOwner;
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace HopFrame.Database.Models;
|
namespace HopFrame.Database.Models;
|
||||||
|
|
||||||
public class PermissionGroup : IPermissionOwner {
|
public class PermissionGroup : IPermissionOwner {
|
||||||
|
|
||||||
|
[Key, Required, MaxLength(50)]
|
||||||
public string Name { get; init; }
|
public string Name { get; init; }
|
||||||
|
|
||||||
|
[Required, DefaultValue(false)]
|
||||||
public bool IsDefaultGroup { get; set; }
|
public bool IsDefaultGroup { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public IList<Permission> Permissions { get; set; }
|
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
36
src/HopFrame.Database/Models/Token.cs
Normal file
36
src/HopFrame.Database/Models/Token.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Models;
|
||||||
|
|
||||||
|
public class Token : IPermissionOwner {
|
||||||
|
public const int RefreshTokenType = 0;
|
||||||
|
public const int AccessTokenType = 1;
|
||||||
|
public const int ApiTokenType = 2;
|
||||||
|
public const int OpenIdTokenType = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the Type of the stored Token
|
||||||
|
/// 0: Refresh token
|
||||||
|
/// 1: Access token
|
||||||
|
/// 2: Api token
|
||||||
|
/// </summary>
|
||||||
|
[Required, MinLength(1), MaxLength(1)]
|
||||||
|
public int Type { get; set; }
|
||||||
|
|
||||||
|
[Key, Required, MinLength(36), MaxLength(36)]
|
||||||
|
public Guid TokenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the creation date of the token
|
||||||
|
/// In case of an api token it defines the date it becomes invalid
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("UserId"), JsonIgnore]
|
||||||
|
public virtual User Owner { get; set; }
|
||||||
|
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
}
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace HopFrame.Database.Models;
|
namespace HopFrame.Database.Models;
|
||||||
|
|
||||||
public sealed class User : IPermissionOwner {
|
public class User : IPermissionOwner {
|
||||||
|
|
||||||
|
[Key, Required]
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
[Required, MaxLength(50)]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
[Required, MaxLength(50), EmailAddress]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[MinLength(8), MaxLength(255), JsonIgnore]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public IList<Permission> Permissions { get; set; }
|
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public virtual List<Token> Tokens { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace HopFrame.Security.Authorization;
|
namespace HopFrame.Database;
|
||||||
|
|
||||||
public static class PermissionValidator {
|
public static class PermissionValidator {
|
||||||
|
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
# HopFrame Database module
|
# HopFrame Database module
|
||||||
This module contains all the logic for the database communication
|
This module contains all the logic for the database communication.
|
||||||
|
|
||||||
|
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||||
|
|||||||
21
src/HopFrame.Database/Repositories/IGroupRepository.cs
Normal file
21
src/HopFrame.Database/Repositories/IGroupRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories;
|
||||||
|
|
||||||
|
public interface IGroupRepository {
|
||||||
|
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||||
|
|
||||||
|
Task<IList<PermissionGroup>> GetDefaultGroups();
|
||||||
|
|
||||||
|
Task<IList<PermissionGroup>> GetUserGroups(User user);
|
||||||
|
|
||||||
|
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||||
|
|
||||||
|
Task EditPermissionGroup(PermissionGroup group);
|
||||||
|
|
||||||
|
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
|
||||||
|
|
||||||
|
Task DeletePermissionGroup(PermissionGroup group);
|
||||||
|
|
||||||
|
internal Task<IList<string>> GetFullGroupPermissions(string group);
|
||||||
|
}
|
||||||
23
src/HopFrame.Database/Repositories/IPermissionRepository.cs
Normal file
23
src/HopFrame.Database/Repositories/IPermissionRepository.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories;
|
||||||
|
|
||||||
|
public interface IPermissionRepository {
|
||||||
|
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// permission system:<br/>
|
||||||
|
/// - "*" -> all rights<br/>
|
||||||
|
/// - "group.[name]" -> group member<br/>
|
||||||
|
/// - "[namespace].[name]" -> single permission<br/>
|
||||||
|
/// - "[namespace].*" -> all permissions in the namespace
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="owner"></param>
|
||||||
|
/// <param name="permission"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
|
||||||
|
|
||||||
|
Task RemovePermission(IPermissionOwner owner, string permission);
|
||||||
|
|
||||||
|
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
|
||||||
|
}
|
||||||
11
src/HopFrame.Database/Repositories/ITokenRepository.cs
Normal file
11
src/HopFrame.Database/Repositories/ITokenRepository.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories;
|
||||||
|
|
||||||
|
public interface ITokenRepository {
|
||||||
|
Task<Token> GetToken(string content);
|
||||||
|
Task<Token> CreateToken(int type, User owner);
|
||||||
|
Task DeleteUserTokens(User owner, bool includeApiTokens = false);
|
||||||
|
Task DeleteToken(Token token);
|
||||||
|
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Security.Models;
|
|
||||||
|
|
||||||
namespace HopFrame.Security.Services;
|
namespace HopFrame.Database.Repositories;
|
||||||
|
|
||||||
public interface IUserService {
|
public interface IUserRepository {
|
||||||
Task<IList<User>> GetUsers();
|
Task<IList<User>> GetUsers();
|
||||||
|
|
||||||
Task<User> GetUser(Guid userId);
|
Task<User> GetUser(Guid userId);
|
||||||
@@ -12,13 +11,8 @@ public interface IUserService {
|
|||||||
|
|
||||||
Task<User> GetUserByUsername(string username);
|
Task<User> GetUserByUsername(string username);
|
||||||
|
|
||||||
Task<User> AddUser(UserRegister user);
|
Task<User> AddUser(User user);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IMPORTANT:<br/>
|
|
||||||
/// This function does not add or remove any permissions to the user.
|
|
||||||
/// For that please use <see cref="IPermissionService"/>
|
|
||||||
/// </summary>
|
|
||||||
Task UpdateUser(User user);
|
Task UpdateUser(User user);
|
||||||
|
|
||||||
Task DeleteUser(User user);
|
Task DeleteUser(User user);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories.Implementation;
|
||||||
|
|
||||||
|
internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase {
|
||||||
|
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
|
||||||
|
return await context.Groups
|
||||||
|
.Include(g => g.Permissions)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<PermissionGroup>> GetDefaultGroups() {
|
||||||
|
return await context.Groups
|
||||||
|
.Include(g => g.Permissions)
|
||||||
|
.Where(g => g.IsDefaultGroup)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IList<PermissionGroup>> GetUserGroups(User user) {
|
||||||
|
return Task.FromResult((IList<PermissionGroup>) context.Groups
|
||||||
|
.Include(g => g.Permissions)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(g => user.Permissions.Any(p => p.PermissionName == g.Name))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PermissionGroup> GetPermissionGroup(string name) {
|
||||||
|
return await context.Groups
|
||||||
|
.Include(g => g.Permissions)
|
||||||
|
.Where(g => g.Name == name)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EditPermissionGroup(PermissionGroup group) {
|
||||||
|
var orig = await context.Groups
|
||||||
|
.Include(g => g.Permissions) // Include related entities
|
||||||
|
.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||||
|
|
||||||
|
if (orig is null) return;
|
||||||
|
|
||||||
|
// Update the main entity's properties
|
||||||
|
orig.IsDefaultGroup = group.IsDefaultGroup;
|
||||||
|
orig.Description = group.Description;
|
||||||
|
|
||||||
|
// Update the permissions
|
||||||
|
foreach (var permission in group.Permissions) {
|
||||||
|
var existingPermission = orig.Permissions.FirstOrDefault(p => p.Id == permission.Id);
|
||||||
|
if (existingPermission != null) {
|
||||||
|
// Update existing permission
|
||||||
|
context.Entry(existingPermission).CurrentValues.SetValues(permission);
|
||||||
|
} else {
|
||||||
|
// Add new permission
|
||||||
|
orig.Permissions.Add(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted permissions
|
||||||
|
foreach (var permission in orig.Permissions.ToList().Where(permission => group.Permissions.All(p => p.Id != permission.Id))) {
|
||||||
|
orig.Permissions.Remove(permission);
|
||||||
|
context.Permissions.Remove(permission); // Ensure it gets removed from the database
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group) {
|
||||||
|
group.CreatedAt = DateTime.Now;
|
||||||
|
await context.Groups.AddAsync(group);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeletePermissionGroup(PermissionGroup group) {
|
||||||
|
context.Groups.Remove(group);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<string>> GetFullGroupPermissions(string group) {
|
||||||
|
var permissions = await context.Permissions
|
||||||
|
.Include(p => p.Group)
|
||||||
|
.Where(p => p.Group != null)
|
||||||
|
.Where(p => p.Group.Name == group)
|
||||||
|
.Select(p => p.PermissionName)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var groups = permissions
|
||||||
|
.Where(p => p.StartsWith("group."))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var subgroup in groups) {
|
||||||
|
permissions.AddRange(await GetFullGroupPermissions(subgroup));
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories.Implementation;
|
||||||
|
|
||||||
|
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
|
||||||
|
public async Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions) {
|
||||||
|
if (owner is Token { Type: Token.ApiTokenType } token) {
|
||||||
|
if (!await HasPermission(token.Owner, permissions)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var perms = (await GetFullPermissions(owner)).ToArray();
|
||||||
|
|
||||||
|
foreach (var permission in permissions) {
|
||||||
|
if (!PermissionValidator.IncludesPermission(permission, perms)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Permission> AddPermission(IPermissionOwner owner, string permission) {
|
||||||
|
var entry = new Permission {
|
||||||
|
GrantedAt = DateTime.Now,
|
||||||
|
PermissionName = permission
|
||||||
|
};
|
||||||
|
|
||||||
|
if (owner is User user) {
|
||||||
|
entry.User = user;
|
||||||
|
}else if (owner is PermissionGroup group) {
|
||||||
|
entry.Group = group;
|
||||||
|
}else if (owner is Token token) {
|
||||||
|
if (token.Type != Token.ApiTokenType)
|
||||||
|
throw new ArgumentException("Only API tokens can have permissions!");
|
||||||
|
if (!await HasPermission(token.Owner, permission))
|
||||||
|
throw new ArgumentException("An api token cannot have more permissions than the owner has!");
|
||||||
|
entry.Token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Permissions.AddAsync(entry);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemovePermission(IPermissionOwner owner, string permission) {
|
||||||
|
Permission entry = null;
|
||||||
|
|
||||||
|
if (owner is User user) {
|
||||||
|
entry = await context.Permissions
|
||||||
|
.Include(p => p.User)
|
||||||
|
.Where(p => p.User != null)
|
||||||
|
.Where(p => p.User.Id == user.Id)
|
||||||
|
.Where(p => p.PermissionName == permission)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}else if (owner is PermissionGroup group) {
|
||||||
|
entry = await context.Permissions
|
||||||
|
.Include(p => p.Group)
|
||||||
|
.Where(p => p.Group != null)
|
||||||
|
.Where(p =>p.Group.Name == group.Name)
|
||||||
|
.Where(p => p.PermissionName == permission)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}else if (owner is Token token) {
|
||||||
|
entry = await context.Permissions
|
||||||
|
.Include(p => p.Token)
|
||||||
|
.Where(p => p.Token != null)
|
||||||
|
.Where(p => p.Token.TokenId == token.TokenId)
|
||||||
|
.Where(p => p.PermissionName == permission)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is not null) {
|
||||||
|
context.Permissions.Remove(entry);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
|
||||||
|
var permissions = new List<string>();
|
||||||
|
|
||||||
|
if (owner is Token token && token.Type != Token.ApiTokenType) {
|
||||||
|
owner = token.Owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owner is User user) {
|
||||||
|
var perms = await context.Permissions
|
||||||
|
.Include(p => p.User)
|
||||||
|
.Where(p => p.User != null)
|
||||||
|
.Where(p => p.User.Id == user.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||||
|
}else if (owner is PermissionGroup group) {
|
||||||
|
var perms = await context.Permissions
|
||||||
|
.Include(p => p.Group)
|
||||||
|
.Where(p => p.Group != null)
|
||||||
|
.Where(p =>p.Group.Name == group.Name)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||||
|
}else if (owner is Token apiToken) {
|
||||||
|
var perms = await context.Permissions
|
||||||
|
.Include(p => p.Token)
|
||||||
|
.Where(p => p.Token != null)
|
||||||
|
.Where(p =>p.Token.TokenId == apiToken.TokenId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = permissions
|
||||||
|
.Where(p => p.StartsWith("group."))
|
||||||
|
.ToList();
|
||||||
|
foreach (var group in groups) {
|
||||||
|
permissions.AddRange(await groupRepository.GetFullGroupPermissions(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories.Implementation;
|
||||||
|
|
||||||
|
internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase {
|
||||||
|
|
||||||
|
public async Task<Token> GetToken(string content) {
|
||||||
|
var success = Guid.TryParse(content, out Guid guid);
|
||||||
|
if (!success) return null;
|
||||||
|
|
||||||
|
return await context.Tokens
|
||||||
|
.Include(t => t.Owner)
|
||||||
|
.Where(t => t.TokenId == guid)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Token> CreateToken(int type, User owner) {
|
||||||
|
var token = new Token {
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
TokenId = Guid.NewGuid(),
|
||||||
|
Type = type,
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.Tokens.AddAsync(token);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
|
||||||
|
var tokens = await context.Tokens
|
||||||
|
.Include(t => t.Owner)
|
||||||
|
.Where(t => t.Owner.Id == owner.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!includeApiTokens)
|
||||||
|
tokens = tokens
|
||||||
|
.Where(t => t.Type != Token.ApiTokenType)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
context.Tokens.RemoveRange(tokens);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteToken(Token token) {
|
||||||
|
context.Tokens.Remove(token);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Token> CreateApiToken(User owner, DateTime expirationDate) {
|
||||||
|
var token = new Token {
|
||||||
|
CreatedAt = expirationDate,
|
||||||
|
TokenId = Guid.NewGuid(),
|
||||||
|
Type = Token.ApiTokenType,
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.Tokens.AddAsync(token);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using HopFrame.Database.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
|
namespace HopFrame.Database.Repositories.Implementation;
|
||||||
|
|
||||||
|
internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase {
|
||||||
|
|
||||||
|
private IIncludableQueryable<User, IList<Token>> IncludeReferences() {
|
||||||
|
return context.Users
|
||||||
|
.Include(u => u.Permissions)
|
||||||
|
.Include(u => u.Tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<User>> GetUsers() {
|
||||||
|
return await IncludeReferences()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> GetUser(Guid userId) {
|
||||||
|
return await IncludeReferences()
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> GetUserByEmail(string email) {
|
||||||
|
return await IncludeReferences()
|
||||||
|
.Where(u => u.Email == email)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> GetUserByUsername(string username) {
|
||||||
|
return await IncludeReferences()
|
||||||
|
.Where(u => u.Username == username)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> AddUser(User user) {
|
||||||
|
if (await GetUserByEmail(user.Email) is not null) return null;
|
||||||
|
if (await GetUserByUsername(user.Username) is not null) return null;
|
||||||
|
|
||||||
|
var entry = new User {
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Email = user.Email,
|
||||||
|
Username = user.Username,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
Permissions = user.Permissions ?? new List<Permission>(),
|
||||||
|
Tokens = user.Tokens
|
||||||
|
};
|
||||||
|
entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
|
var defaultGroups = await groupRepository.GetDefaultGroups();
|
||||||
|
foreach (var group in defaultGroups) {
|
||||||
|
entry.Permissions.Add(new Permission {
|
||||||
|
PermissionName = group.Name,
|
||||||
|
GrantedAt = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Users.AddAsync(entry);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUser(User user) {
|
||||||
|
var entry = await IncludeReferences()
|
||||||
|
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||||
|
if (entry is null) return;
|
||||||
|
|
||||||
|
// Update the main entity's properties
|
||||||
|
entry.Email = user.Email;
|
||||||
|
entry.Username = user.Username;
|
||||||
|
|
||||||
|
// Update Permissions
|
||||||
|
foreach (var permission in user.Permissions) {
|
||||||
|
var existingPermission = entry.Permissions.FirstOrDefault(p => p.Id == permission.Id);
|
||||||
|
if (existingPermission != null) {
|
||||||
|
// Update existing permission
|
||||||
|
context.Entry(existingPermission).CurrentValues.SetValues(permission);
|
||||||
|
} else {
|
||||||
|
// Add new permission
|
||||||
|
entry.Permissions.Add(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted permissions
|
||||||
|
foreach (var permission in entry.Permissions.ToList().Where(permission => user.Permissions.All(p => p.Id != permission.Id))) {
|
||||||
|
entry.Permissions.Remove(permission);
|
||||||
|
context.Permissions.Remove(permission); // Ensure it gets removed from the database
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Tokens
|
||||||
|
foreach (var token in user.Tokens) {
|
||||||
|
var existingToken = entry.Tokens.FirstOrDefault(t => t.TokenId == token.TokenId);
|
||||||
|
if (existingToken != null) {
|
||||||
|
// Update existing token
|
||||||
|
context.Entry(existingToken).CurrentValues.SetValues(token);
|
||||||
|
} else {
|
||||||
|
// Add new token
|
||||||
|
entry.Tokens.Add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted tokens
|
||||||
|
foreach (var token in entry.Tokens.ToList().Where(token => user.Tokens.All(t => t.TokenId != token.TokenId))) {
|
||||||
|
entry.Tokens.Remove(token);
|
||||||
|
context.Tokens.Remove(token); // Ensure it gets removed from the database
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUser(User user) {
|
||||||
|
var entry = await context.Users
|
||||||
|
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||||
|
|
||||||
|
if (entry is null) return;
|
||||||
|
|
||||||
|
context.Users.Remove(entry);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckUserPassword(User user, string password) {
|
||||||
|
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
|
var entry = await context.Users
|
||||||
|
.Where(entry => entry.Id == user.Id)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
|
return entry.Password == hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangePassword(User user, string password) {
|
||||||
|
var entry = await context.Users
|
||||||
|
.Where(entry => entry.Id == user.Id)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
|
if (entry is null) return;
|
||||||
|
|
||||||
|
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
entry.Password = hash;
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/HopFrame.Database/ServiceCollectionExtensions.cs
Normal file
18
src/HopFrame.Database/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Database.Repositories.Implementation;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Database;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions {
|
||||||
|
|
||||||
|
public static IServiceCollection AddHopFrameRepositories<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||||
|
services.AddScoped<IGroupRepository, GroupRepository<TDbContext>>();
|
||||||
|
services.AddScoped<IPermissionRepository, PermissionRepository<TDbContext>>();
|
||||||
|
services.AddScoped<IUserRepository, UserRepository<TDbContext>>();
|
||||||
|
services.AddScoped<ITokenRepository, TokenRepository<TDbContext>>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using HopFrame.Database;
|
using HopFrame.Database.Models;
|
||||||
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Services;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -13,45 +14,89 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace HopFrame.Security.Authentication;
|
namespace HopFrame.Security.Authentication;
|
||||||
|
|
||||||
public class HopFrameAuthentication<TDbContext>(
|
public class HopFrameAuthentication(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
ISystemClock clock,
|
ISystemClock clock,
|
||||||
TDbContext context,
|
ITokenRepository tokens,
|
||||||
IPermissionService perms)
|
IPermissionRepository perms,
|
||||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)
|
IOptions<HopFrameAuthenticationOptions> tokenOptions,
|
||||||
where TDbContext : HopDbContextBase {
|
IOptions<OpenIdOptions> openIdOptions,
|
||||||
|
IUserRepository users,
|
||||||
|
IOpenIdAccessor accessor)
|
||||||
|
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||||
|
|
||||||
public const string SchemeName = "HopCore.Authentication";
|
public const string SchemeName = "HopFrame.Authentication";
|
||||||
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
|
|
||||||
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
|
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||||
var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
|
var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
|
||||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
|
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
|
||||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||||
|
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
||||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||||
|
|
||||||
var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
|
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 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 (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId))
|
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");
|
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||||
|
|
||||||
|
var principal = await GenerateClaims(tokenEntry, perms);
|
||||||
|
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ClaimsPrincipal> GenerateClaims(Token token, IPermissionRepository perms) {
|
||||||
var claims = new List<Claim> {
|
var claims = new List<Claim> {
|
||||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||||
new(HopFrameClaimTypes.UserId, tokenEntry.UserId)
|
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
var permissions = await perms.GetFullPermissions(tokenEntry.UserId);
|
var permissions = await perms.GetFullPermissions(token);
|
||||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||||
|
|
||||||
var principal = new ClaimsPrincipal();
|
var principal = new ClaimsPrincipal();
|
||||||
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
||||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
return principal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,37 @@
|
|||||||
using HopFrame.Database;
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
|
using HopFrame.Security.Authorization;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Services;
|
using HopFrame.Security.Options;
|
||||||
using HopFrame.Security.Services.Implementation;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
namespace HopFrame.Security.Authentication;
|
namespace HopFrame.Security.Authentication;
|
||||||
|
|
||||||
public static class HopFrameAuthenticationExtensions {
|
public static class HopFrameAuthenticationExtensions {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
|
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="service">The service provider to add the services to</param>
|
/// <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>
|
/// <returns></returns>
|
||||||
public static IServiceCollection AddHopFrameAuthentication<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase {
|
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) {
|
||||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>();
|
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||||
service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
|
|
||||||
service.AddScoped<IUserService, UserService<TDbContext>>();
|
|
||||||
|
|
||||||
service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {});
|
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();
|
service.AddAuthorization();
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using HopFrame.Database;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ public interface ITokenContext {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The access token the user provided
|
/// The access token the user provided
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Guid AccessToken { get; }
|
Token AccessToken { get; }
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
using HopFrame.Database;
|
|
||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace HopFrame.Security.Claims;
|
namespace HopFrame.Security.Claims;
|
||||||
|
|
||||||
internal sealed class TokenContextImplementor<TDbContext>(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase {
|
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext {
|
||||||
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
||||||
|
|
||||||
public User User => context.Users
|
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
|
||||||
.SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())?
|
|
||||||
.ToUserModel(context);
|
|
||||||
|
|
||||||
public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString());
|
public Token AccessToken => options.Value.Enabled ? new Token {
|
||||||
|
Owner = User,
|
||||||
|
Type = Token.OpenIdTokenType,
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
<RootNamespace>HopFrame.Security</RootNamespace>
|
<RootNamespace>HopFrame.Security</RootNamespace>
|
||||||
|
|
||||||
<PackageId>HopFrame.Security</PackageId>
|
<PackageId>HopFrame.Security</PackageId>
|
||||||
<Version>1.1.0</Version>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace HopFrame.Security.Options;
|
||||||
|
|
||||||
|
public abstract class OptionsFromConfiguration {
|
||||||
|
public abstract string Position { get; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +1,4 @@
|
|||||||
# HopFrame Security module
|
# HopFrame Security module
|
||||||
this module contains all handlers for the login and register validation. It also checks the user permissions.
|
this module contains all handlers for the login and register validation. It also checks the user permissions.
|
||||||
|
|
||||||
# Services added in this module
|
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
|
||||||
|
|
||||||
## ITokenContext
|
|
||||||
This service provides the information given by the current request
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface ITokenContext {
|
|
||||||
bool IsAuthenticated { get; }
|
|
||||||
|
|
||||||
User User { get; }
|
|
||||||
|
|
||||||
Guid AccessToken { get; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## IUserService
|
|
||||||
This service simplifies the data access of the user table in the database.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IUserService {
|
|
||||||
Task<IList<User>> GetUsers();
|
|
||||||
|
|
||||||
Task<User> GetUser(Guid userId);
|
|
||||||
|
|
||||||
Task<User> GetUserByEmail(string email);
|
|
||||||
|
|
||||||
Task<User> GetUserByUsername(string username);
|
|
||||||
|
|
||||||
Task<User> AddUser(UserRegister user);
|
|
||||||
|
|
||||||
Task UpdateUser(User user);
|
|
||||||
|
|
||||||
Task DeleteUser(User user);
|
|
||||||
|
|
||||||
Task<bool> CheckUserPassword(User user, string password);
|
|
||||||
|
|
||||||
Task ChangePassword(User user, string password);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## IPermissionService
|
|
||||||
This service handles all permission and group interactions with the data source.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface IPermissionService {
|
|
||||||
Task<bool> HasPermission(string permission, Guid user);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
|
||||||
|
|
||||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
|
||||||
|
|
||||||
Task EditPermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
|
||||||
|
|
||||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
|
||||||
|
|
||||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
|
||||||
|
|
||||||
Task DeletePermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
|
||||||
|
|
||||||
Task AddPermission(IPermissionOwner owner, string permission);
|
|
||||||
|
|
||||||
Task RemovePermission(Permission permission);
|
|
||||||
|
|
||||||
Task<string[]> GetFullPermissions(string user);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
using HopFrame.Database.Models;
|
|
||||||
|
|
||||||
namespace HopFrame.Security.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// permission system:<br/>
|
|
||||||
/// - "*" -> all rights<br/>
|
|
||||||
/// - "group.[name]" -> group member<br/>
|
|
||||||
/// - "[namespace].[name]" -> single permission<br/>
|
|
||||||
/// - "[namespace].*" -> all permissions in the namespace
|
|
||||||
/// </summary>
|
|
||||||
public interface IPermissionService {
|
|
||||||
|
|
||||||
Task<bool> HasPermission(string permission, Guid user);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
|
||||||
|
|
||||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
|
||||||
|
|
||||||
Task EditPermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
|
||||||
|
|
||||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
|
||||||
|
|
||||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
|
||||||
|
|
||||||
Task DeletePermissionGroup(PermissionGroup group);
|
|
||||||
|
|
||||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// permission system:<br/>
|
|
||||||
/// - "*" -> all rights<br/>
|
|
||||||
/// - "group.[name]" -> group member<br/>
|
|
||||||
/// - "[namespace].[name]" -> single permission<br/>
|
|
||||||
/// - "[namespace].*" -> all permissions in the namespace
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="owner"></param>
|
|
||||||
/// <param name="permission"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task AddPermission(IPermissionOwner owner, string permission);
|
|
||||||
|
|
||||||
Task RemovePermission(Permission permission);
|
|
||||||
|
|
||||||
Task<string[]> GetFullPermissions(string user);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
using HopFrame.Database;
|
|
||||||
using HopFrame.Database.Models;
|
|
||||||
using HopFrame.Database.Models.Entries;
|
|
||||||
using HopFrame.Security.Authorization;
|
|
||||||
using HopFrame.Security.Claims;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace HopFrame.Security.Services.Implementation;
|
|
||||||
|
|
||||||
internal sealed class PermissionService<TDbContext>(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase {
|
|
||||||
public async Task<bool> HasPermission(string permission) {
|
|
||||||
return await HasPermission(permission, current.User.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> HasPermissions(params string[] permissions) {
|
|
||||||
var user = current.User.Id.ToString();
|
|
||||||
var perms = await GetFullPermissions(user);
|
|
||||||
|
|
||||||
foreach (var permission in permissions) {
|
|
||||||
if (!PermissionValidator.IncludesPermission(permission, perms)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> HasAnyPermission(params string[] permissions) {
|
|
||||||
var user = current.User.Id.ToString();
|
|
||||||
var perms = await GetFullPermissions(user);
|
|
||||||
|
|
||||||
foreach (var permission in permissions) {
|
|
||||||
if (PermissionValidator.IncludesPermission(permission, perms)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> HasPermission(string permission, Guid user) {
|
|
||||||
var permissions = await GetFullPermissions(user.ToString());
|
|
||||||
|
|
||||||
return PermissionValidator.IncludesPermission(permission, permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
|
|
||||||
return await context.Groups
|
|
||||||
.Select(group => group.ToPermissionGroup(context))
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<PermissionGroup> GetPermissionGroup(string name) {
|
|
||||||
return context.Groups
|
|
||||||
.Where(group => group.Name == name)
|
|
||||||
.Select(group => group.ToPermissionGroup(context))
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task EditPermissionGroup(PermissionGroup group) {
|
|
||||||
var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name);
|
|
||||||
|
|
||||||
if (orig is null) return;
|
|
||||||
|
|
||||||
var entity = context.Groups.Update(orig);
|
|
||||||
|
|
||||||
entity.Entity.Default = group.IsDefaultGroup;
|
|
||||||
entity.Entity.Description = group.Description;
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<PermissionGroup>> GetUserPermissionGroups(User user) {
|
|
||||||
var groups = await context.Groups.ToListAsync();
|
|
||||||
var perms = await GetFullPermissions(user.Id.ToString());
|
|
||||||
|
|
||||||
return groups
|
|
||||||
.Where(group => perms.Contains(group.Name))
|
|
||||||
.Select(group => group.ToPermissionGroup(context))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveGroupFromUser(User user, PermissionGroup group) {
|
|
||||||
var entry = await context.Permissions
|
|
||||||
.Where(perm => perm.PermissionText == group.Name && perm.UserId == user.Id.ToString())
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
|
|
||||||
if (entry is null) return;
|
|
||||||
|
|
||||||
context.Permissions.Remove(entry);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null) {
|
|
||||||
var group = new GroupEntry {
|
|
||||||
Name = name,
|
|
||||||
Description = description,
|
|
||||||
Default = isDefault,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.Groups.AddAsync(group);
|
|
||||||
|
|
||||||
if (isDefault) {
|
|
||||||
var users = await context.Users.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var user in users) {
|
|
||||||
await context.Permissions.AddAsync(new PermissionEntry {
|
|
||||||
GrantedAt = DateTime.Now,
|
|
||||||
PermissionText = group.Name,
|
|
||||||
UserId = user.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return group.ToPermissionGroup(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeletePermissionGroup(PermissionGroup group) {
|
|
||||||
var entry = await context.Groups.SingleOrDefaultAsync(entry => entry.Name == group.Name);
|
|
||||||
context.Groups.Remove(entry);
|
|
||||||
|
|
||||||
var permissions = await context.Permissions
|
|
||||||
.Where(perm => perm.UserId == group.Name || perm.PermissionText == group.Name)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (permissions.Count > 0) {
|
|
||||||
context.Permissions.RemoveRange(permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Permission> GetPermission(string name, IPermissionOwner owner) {
|
|
||||||
var ownerId = (owner is User user) ? user.Id.ToString() : ((PermissionGroup)owner).Name;
|
|
||||||
|
|
||||||
return await context.Permissions
|
|
||||||
.Where(perm => perm.PermissionText == name && perm.UserId == ownerId)
|
|
||||||
.Select(perm => perm.ToPermissionModel())
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddPermission(IPermissionOwner owner, string permission) {
|
|
||||||
var userId = owner is User user ? user.Id.ToString() : (owner as PermissionGroup)?.Name;
|
|
||||||
|
|
||||||
await context.Permissions.AddAsync(new PermissionEntry {
|
|
||||||
UserId = userId,
|
|
||||||
PermissionText = permission,
|
|
||||||
GrantedAt = DateTime.Now
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemovePermission(Permission permission) {
|
|
||||||
var entry = await context.Permissions.SingleOrDefaultAsync(entry => entry.RecordId == permission.Id);
|
|
||||||
context.Permissions.Remove(entry);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string[]> GetFullPermissions(string user) {
|
|
||||||
var permissions = await context.Permissions
|
|
||||||
.Where(perm => perm.UserId == user)
|
|
||||||
.Select(perm => perm.PermissionText)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var groups = permissions
|
|
||||||
.Where(perm => perm.StartsWith("group."))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var groupPerms = new List<string>();
|
|
||||||
foreach (var group in groups) {
|
|
||||||
var perms = await GetFullPermissions(group);
|
|
||||||
groupPerms.AddRange(perms);
|
|
||||||
}
|
|
||||||
|
|
||||||
permissions.AddRange(groupPerms);
|
|
||||||
|
|
||||||
return permissions.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using HopFrame.Database;
|
|
||||||
using HopFrame.Database.Models;
|
|
||||||
using HopFrame.Database.Models.Entries;
|
|
||||||
using HopFrame.Security.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace HopFrame.Security.Services.Implementation;
|
|
||||||
|
|
||||||
internal sealed class UserService<TDbContext>(TDbContext context) : IUserService where TDbContext : HopDbContextBase {
|
|
||||||
public async Task<IList<User>> GetUsers() {
|
|
||||||
return await context.Users
|
|
||||||
.Select(user => user.ToUserModel(context))
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User> GetUser(Guid userId) {
|
|
||||||
var id = userId.ToString();
|
|
||||||
|
|
||||||
return context.Users
|
|
||||||
.Where(user => user.Id == id)
|
|
||||||
.Select(user => user.ToUserModel(context))
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User> GetUserByEmail(string email) {
|
|
||||||
return context.Users
|
|
||||||
.Where(user => user.Email == email)
|
|
||||||
.Select(user => user.ToUserModel(context))
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User> GetUserByUsername(string username) {
|
|
||||||
return context.Users
|
|
||||||
.Where(user => user.Username == username)
|
|
||||||
.Select(user => user.ToUserModel(context))
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User> AddUser(UserRegister user) {
|
|
||||||
if (await GetUserByEmail(user.Email) is not null) return null;
|
|
||||||
if (await GetUserByUsername(user.Username) is not null) return null;
|
|
||||||
|
|
||||||
var entry = new UserEntry {
|
|
||||||
Id = Guid.NewGuid().ToString(),
|
|
||||||
Email = user.Email,
|
|
||||||
Username = user.Username,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
|
||||||
|
|
||||||
await context.Users.AddAsync(entry);
|
|
||||||
|
|
||||||
var defaultGroups = await context.Groups
|
|
||||||
.Where(group => group.Default)
|
|
||||||
.Select(group => "group." + group.Name)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
await context.Permissions.AddRangeAsync(defaultGroups.Select(group => new PermissionEntry {
|
|
||||||
GrantedAt = DateTime.Now,
|
|
||||||
PermissionText = group,
|
|
||||||
UserId = entry.Id
|
|
||||||
}));
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return entry.ToUserModel(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUser(User user) {
|
|
||||||
var id = user.Id.ToString();
|
|
||||||
var entry = await context.Users
|
|
||||||
.SingleOrDefaultAsync(entry => entry.Id == id);
|
|
||||||
if (entry is null) return;
|
|
||||||
|
|
||||||
entry.Email = user.Email;
|
|
||||||
entry.Username = user.Username;
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteUser(User user) {
|
|
||||||
var id = user.Id.ToString();
|
|
||||||
var entry = await context.Users
|
|
||||||
.SingleOrDefaultAsync(entry => entry.Id == id);
|
|
||||||
|
|
||||||
if (entry is null) return;
|
|
||||||
|
|
||||||
context.Users.Remove(entry);
|
|
||||||
|
|
||||||
var userTokens = await context.Tokens
|
|
||||||
.Where(token => token.UserId == id)
|
|
||||||
.ToArrayAsync();
|
|
||||||
context.Tokens.RemoveRange(userTokens);
|
|
||||||
|
|
||||||
var userPermissions = await context.Permissions
|
|
||||||
.Where(perm => perm.UserId == id)
|
|
||||||
.ToArrayAsync();
|
|
||||||
context.Permissions.RemoveRange(userPermissions);
|
|
||||||
|
|
||||||
context.OnUserDelete(entry);
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CheckUserPassword(User user, string password) {
|
|
||||||
var id = user.Id.ToString();
|
|
||||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
|
||||||
|
|
||||||
var entry = await context.Users
|
|
||||||
.Where(entry => entry.Id == id)
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
|
|
||||||
return entry.Password == hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ChangePassword(User user, string password) {
|
|
||||||
var entry = await context.Users
|
|
||||||
.Where(entry => entry.Id == user.Id.ToString())
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
|
|
||||||
if (entry is null) return;
|
|
||||||
|
|
||||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
|
||||||
entry.Password = hash;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
public abstract class AdminPagesContext {
|
||||||
|
|
||||||
|
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
|
||||||
|
|
||||||
|
}
|
||||||
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||||
|
public sealed class AdminNameAttribute(string name) : Attribute {
|
||||||
|
public string Name { get; set; } = name;
|
||||||
|
}
|
||||||
10
src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs
Normal file
10
src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This attribute specifies the url of the admin page and needs to be applied on the AdminPage property in the AdminContext directly
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The page url: '/administration/{url}'</param>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminPageUrlAttribute(string url) : Attribute {
|
||||||
|
public string Url { get; set; } = url;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
|
||||||
|
public bool ShowCreateButton { get; set; } = showCreateButton;
|
||||||
|
public bool ShowDeleteButton { get; set; } = showDeleteButton;
|
||||||
|
public bool ShowUpdateButton { get; set; } = showUpdateButton;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminDescriptionAttribute(string description) : Attribute {
|
||||||
|
public string Description { get; set; } = description;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
|
||||||
|
public AdminPagePermissions Permissions { get; set; } = new() {
|
||||||
|
Create = create,
|
||||||
|
Update = update,
|
||||||
|
Delete = delete,
|
||||||
|
Read = view
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user