Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c64497a08 | |||
|
|
d9fec99954 | ||
|
|
dffe653a44 |
@@ -17,23 +17,18 @@ build:
|
||||
artifacts:
|
||||
paths:
|
||||
- "**/bin/Release"
|
||||
expire_in: 10 minutes
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- dotnet test --verbosity normal
|
||||
dependencies:
|
||||
- build
|
||||
- dotnet test --no-restore --verbosity normal
|
||||
|
||||
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
|
||||
- dotnet pack -c Release -o .
|
||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json; done
|
||||
only:
|
||||
- tags
|
||||
dependencies:
|
||||
- build
|
||||
- test
|
||||
- main
|
||||
variables:
|
||||
NUGET_API_KEY: $NUGET_API_KEY
|
||||
|
||||
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>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db</jdbc-url>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
|
||||
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
<option name="applicationTheme" value="default" />
|
||||
<option name="iconsTheme" value="default" />
|
||||
<option name="button1Title" value="" />
|
||||
<option name="button1Url" value="" />
|
||||
<option name="button2Title" value="" />
|
||||
<option name="button2Url" value="" />
|
||||
<option name="customApplicationId" value="" />
|
||||
</component>
|
||||
</project>
|
||||
45
HopFrame.sln
45
HopFrame.sln
@@ -2,7 +2,7 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTest", "test\RestApiTest\RestApiTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
|
||||
EndProject
|
||||
@@ -10,24 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Web", "testing\HopFrame.Testing.Web\HopFrame.Testing.Web.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{64EDCBED-A84F-4936-8697-78DC43CB2427}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{EEA20D27-D471-44AF-A287-C0E068D93182}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1D98E5DE-CB8B-4C1C-A319-D49AC137441A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Database", "tests\HopFrame.Tests.Database\HopFrame.Tests.Database.csproj", "{1CAAC943-B8FE-48DD-9712-92699647DE18}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Security", "tests\HopFrame.Tests.Security\HopFrame.Tests.Security.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Api", "tests\HopFrame.Tests.Api\HopFrame.Tests.Api.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -62,34 +48,7 @@ Global
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{003120AE-F38B-4632-8497-BE4505189627} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{7F82E1C6-4A42-4337-9E03-2EE6429D004F} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{3BE585BC-13A5-4BE4-A806-E9EC2D825956} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||
{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{6747753A-6059-48F1-B779-D73765A373A6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,86 +1,8 @@
|
||||
<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>
|
||||
<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" />
|
||||
</AssemblyExplorer></s:String>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</wpf:ResourceDictionary>
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
||||
@@ -6,8 +6,6 @@ A simple backend management api for ASP.NET Core Web APIs
|
||||
- [x] User authentication
|
||||
- [x] Permission management
|
||||
- [x] Generated frontend administration boards
|
||||
- [x] API token support
|
||||
- [x] OpenID authentication integration
|
||||
|
||||
# Usage
|
||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# HopFrame Authentication
|
||||
|
||||
HopFrame uses a token system with a short term access token and a long term refresh token for authenticating users.
|
||||
These tokens are usually provided to the endpoints of the API / Blazor Pages through Cookies:
|
||||
|
||||
| Cookie key | Cookie value sample | Description |
|
||||
|--------------------------------|----------------------------------------|-----------------------------|
|
||||
| HopFrame.Security.RefreshToken | `42047983-914d-418b-841a-4382614231be` | The long term refresh token |
|
||||
| HopFrame.Security.AccessToken | `d39c9432-0831-42df-8844-5e2b70f03eda` | The short term access token |
|
||||
|
||||
The advantage of these cookies is that they are automatically set by the backend and delete themselves, when they are
|
||||
no longer valid.
|
||||
|
||||
The access token can also be delivered through a header called `HopFrame.Authentication` or `Token`.
|
||||
It can also be delivered through a query parameter called `token`. This simplifies requests for images for example
|
||||
because you can directly specify the url in the img tag in html.
|
||||
|
||||
## Authentication configuration
|
||||
|
||||
You can also configure the time span that the tokens are valid using the `appsettings.json` or environment variables
|
||||
by configuring your configuration to load these.
|
||||
>**Hint**: Configuring your application to use environment variables works by simply adding
|
||||
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
||||
> custom configurations / HopFrame services.
|
||||
|
||||
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
||||
These get combined to a single time span. You can also completely disable the default authentication
|
||||
by setting the `DefaultAuthentication` to `false`. Note that you will no longer be able to login in any
|
||||
way unless you enabled the [OpenID](./openid.md) authentication.
|
||||
|
||||
#### Configuration example
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"AccessToken": {
|
||||
"Minutes": 30
|
||||
},
|
||||
"RefreshToken": {
|
||||
"Days": 10,
|
||||
"Hours": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment variables example
|
||||
```dotenv
|
||||
HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30
|
||||
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10
|
||||
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
|
||||
```
|
||||
|
||||
## API tokens
|
||||
|
||||
API tokens are useful to use in automation environments that need to access an endpoint or page of your application.
|
||||
The HopFrame supports this natively and no further configuration is required in order to use them.
|
||||
|
||||
### Create an api token
|
||||
|
||||
You can create an api token via the `ITokenRepository`:
|
||||
```csharp
|
||||
tokens.CreateApiToken(user, DateTime.MaxValue);
|
||||
```
|
||||
|
||||
This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token
|
||||
model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default
|
||||
has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that an api token
|
||||
can **never** have more permissions than the user associated with it.
|
||||
|
||||
### Add permissions to an api token
|
||||
|
||||
You can add permissions to an api token like you would to a normal user or group:
|
||||
|
||||
```csharp
|
||||
permissions.AddPermission(apiToken, "token.permission");
|
||||
```
|
||||
@@ -35,18 +35,16 @@ public class Permission {
|
||||
public DateTime GrantedAt { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
public virtual Token Token { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## Token
|
||||
```csharp
|
||||
public class Token : IPermissionOwner {
|
||||
public class Token {
|
||||
public int Type { get; set; }
|
||||
public Guid Content { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public virtual User Owner { get; set; }
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
120
docs/openid.md
120
docs/openid.md
@@ -1,120 +0,0 @@
|
||||
# OpenID Authentication
|
||||
The HopFrame allows you to use an OpenID provider as your authentication provider for single sign on or better security
|
||||
etc. To use it, you just simply need to configure it through the `appsettings.json` or environment variables.
|
||||
|
||||
>**Note**: The Blazor module has not yet implemented endpoints for the login process, but the middleware is correctly
|
||||
> configured and the `IOpenIdAccessor` service is also provided for you to easily implement the endpoints yourself.
|
||||
|
||||
When you have enabled the integration, new endpoints will also be provided to perform the authentication.
|
||||
simply use the swagger explorer to look up how the endpoints function. They're all under the subroute
|
||||
`/api/v1/openid/`.
|
||||
|
||||
## Configure the HopFrame to use OpenID authentication
|
||||
|
||||
1. Create / Configure your OpenID provider:
|
||||
|
||||
- Save the ClientID and Client Secret from the provider, because you need it later.
|
||||
- The default redirect uri looks something like this: `https://example.com/api/v1/openid/callback`.
|
||||
- **Replace** the origin with the FQDN of your service.
|
||||
- In order for the HopFrame to automatically renew expired access tokens you need to enable the `offline_access` scope.
|
||||
- The integration also works without doing that, but then you need to reauthenticate every time your access token expires.
|
||||
|
||||
2. Configure the HopFrame integration:
|
||||
|
||||
>**Hint**: All of these configuration options can also be defined as environment variables. Use '__'
|
||||
> to separate the namespaces like so: `HOPFRAME__AUTHENTICATION__OPENID__ENABLED=true`
|
||||
|
||||
- Add the following lines to your `appsettings.json`:
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Enabled": true,
|
||||
"Issuer": "your-issuer",
|
||||
"ClientId": "your-client-id",
|
||||
"ClientSecret": "your-client-secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
>**Hint**: If you are using Authentik, the issuer url looks something like this: `https://auth.example.com/application/o/application-name/`.
|
||||
> Just replace the FQDN and application-name with your configured application.
|
||||
|
||||
- **Optional**: You can also disable the default authentication via the config:
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"DefaultAuthentication": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: By default, the HopFrame will cache the api responses to reduce api latency. This can also be configured in the config (the cache can also be completely disabled here):
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Cache": {
|
||||
"Enabled": true,
|
||||
"Configuration": {
|
||||
"Hours": 5
|
||||
},
|
||||
"Auth": {
|
||||
"Seconds": 90
|
||||
},
|
||||
"Inspection": {
|
||||
"Minutes": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: You can also define your own callback endpoint like so (you also need to add / replace the endpoint in the provider settings):
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Callback": "https://example.com/auth/callback"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: You can also prevent new users from being created by disabling it in the config:
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"GenerateUsers": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use the abstraction to integrate OpenID yourself
|
||||
|
||||
The HopFrame has a service, that simplifies the communication with the OpenID provider called `IOpenIdAccessor`.
|
||||
You can inject it like every other service in your application.
|
||||
|
||||
```csharp
|
||||
public interface IOpenIdAccessor {
|
||||
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
|
||||
Task<OpenIdToken> RequestToken(string code, string defaultCallback);
|
||||
|
||||
Task<string> ConstructAuthUri(string defaultCallback, string state = null);
|
||||
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
|
||||
}
|
||||
```
|
||||
@@ -1,80 +0,0 @@
|
||||
# HopFrame Permissions
|
||||
|
||||
Permissions in the HopFrame are simple and effective to use.
|
||||
As discussed in the [repositories](./repositories.md) documentation, you can manage user / group permissions
|
||||
via the `IPermissionRepository` service.
|
||||
|
||||
## How do permissions work in the HopFrame
|
||||
|
||||
Permissions are defined using the . (dot) syntax. This enables you to nest permissions in namespaces.
|
||||
You can also give a user or a group the permission to every permission in a namespace by using the * (star) syntax.
|
||||
|
||||
| Permission | Example | Description |
|
||||
|----------------------|-------------------------------|-------------------------------------------------------|
|
||||
| `*` | `*` | all permissions |
|
||||
| `[namespace].[name]` | `hopframe.admin.users.create` | single permission |
|
||||
| `[namespace].*` | `hopframe.admin.*` | all permissions in that namespace (works recursively) |
|
||||
|
||||
### Reserved namespaces
|
||||
|
||||
| Namespace | Example | Description |
|
||||
|-----------|---------------|------------------------------------------|
|
||||
| `group` | `group.admin` | The user needs to be in a specific group |
|
||||
|
||||
### Permission Groups
|
||||
|
||||
You can manage them through the `IGroupRepository` as described in the [repositories](./repositories.md) documentation.
|
||||
You add permissions just like you would to a user with the `IPermissionRepository`.
|
||||
You can assign a user to a group by assigning the group permission to the user:
|
||||
```csharp
|
||||
permissionRepository.AddPermission(user, "group.admin");
|
||||
```
|
||||
|
||||
## Predefined Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|--------------------------------|-------------------------------|
|
||||
| `hopframe.admin` | Access to the admin dashboard |
|
||||
| `hopframe.admin.users.read` | View all users |
|
||||
| `hopframe.admin.users.update` | Edit a user |
|
||||
| `hopframe.admin.users.delete` | Delete a user |
|
||||
| `hopframe.admin.users.create` | Add a group |
|
||||
| `hopframe.admin.groups.read` | View all groups |
|
||||
| `hopframe.admin.groups.update` | Edit a group |
|
||||
| `hopframe.admin.groups.delete` | Delete a group |
|
||||
| `hopframe.admin.groups.create` | Add a group |
|
||||
|
||||
### Configuring HopFrame permissions
|
||||
|
||||
You can also configure the predefined permissions using the `appsettings.json` or environment variables
|
||||
by configuring your configuration to load these.
|
||||
>**Hint**: Configuring your application to use environment variables works by simply adding
|
||||
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
||||
> custom configurations / HopFrame services.
|
||||
|
||||
You can specify `Dashboard` for the dashboard permission and for `Users` and `Groups` you can specify
|
||||
`Create`, `Read`, `Update` and `Delete` permissions.
|
||||
|
||||
#### Configuration example
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Permissions": {
|
||||
"Dashboard": "myapp.dashboard.view",
|
||||
"Users": {
|
||||
"Read": "myapp.read.users"
|
||||
},
|
||||
"Groups": {
|
||||
"Create": "myapp.create.groups",
|
||||
"Update": "myapp.update.groups"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment variables example
|
||||
```dotenv
|
||||
HOPFRAME__PERMISSIONS__DASHBOARD="myapp.dashboard.view"
|
||||
HOPFRAME__PERMISSIONS__USERS__READ="myapp.read.users"
|
||||
HOPFRAME__PERMISSIONS__GROUPS__CREATE="myapp.create.groups"
|
||||
HOPFRAME__PERMISSIONS__GROUPS__UPDATE="myapp.update.groups"
|
||||
```
|
||||
@@ -7,9 +7,6 @@ The HopFrame comes in two variations, you can eiter only use the backend with so
|
||||
- [Database](./database.md)
|
||||
- [Repositories](./repositories.md)
|
||||
- [Base Models](./models.md)
|
||||
- [Authentication](./authentication.md)
|
||||
- [Permissions](./permissions.md)
|
||||
- [OpenID Integration](./openid.md)
|
||||
|
||||
## HopFrame Web API
|
||||
|
||||
|
||||
@@ -71,9 +71,5 @@ public interface ITokenRepository {
|
||||
Task<Token> CreateToken(int type, User owner);
|
||||
|
||||
Task DeleteUserTokens(User owner);
|
||||
|
||||
Task DeleteToken(Token token);
|
||||
|
||||
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/groups")]
|
||||
public class GroupController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IGroupLogic groups) : ControllerBase {
|
||||
|
||||
private async Task<bool> AuthorizeRequest(string permission) {
|
||||
return await perms.HasPermission(context.AccessToken, permission);
|
||||
}
|
||||
|
||||
[HttpGet, Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetGroups() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetGroups();
|
||||
}
|
||||
|
||||
[HttpGet("default"), Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetDefaultGroups() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetDefaultGroups();
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}"), Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetUserGroups(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetUserGroups(userId);
|
||||
}
|
||||
|
||||
[HttpGet("{name}"), Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> GetGroup(string name) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetGroup(name);
|
||||
}
|
||||
|
||||
[HttpPost, Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> CreateGroup([FromBody] PermissionGroup group) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Create))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.CreateGroup(group);
|
||||
}
|
||||
|
||||
[HttpPut, Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> UpdateGroup([FromBody] PermissionGroup group) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.UpdateGroup(group);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}"), Authorized]
|
||||
public async Task<ActionResult> DeleteGroup(string name) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Delete))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.DeleteGroup(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/openid")]
|
||||
public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
|
||||
public const string DefaultCallback = "api/v1/openid/callback";
|
||||
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) {
|
||||
var uri = await accessor.ConstructAuthUri(redirectAfter);
|
||||
|
||||
if (performRedirect == 1) {
|
||||
return Redirect(uri);
|
||||
}
|
||||
|
||||
return Ok(new SingleValueResult<string>(uri));
|
||||
}
|
||||
|
||||
[HttpGet("callback")]
|
||||
public async Task<IActionResult> Callback([FromQuery] string code, [FromQuery] string state) {
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
return BadRequest("Authorization code is missing");
|
||||
}
|
||||
|
||||
var token = await accessor.RequestToken(code);
|
||||
|
||||
if (token is null) {
|
||||
return Forbid("Authorization code is not valid");
|
||||
}
|
||||
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
if (string.IsNullOrEmpty(state)) {
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
}
|
||||
|
||||
return Redirect(state.Replace("{token}", token.AccessToken));
|
||||
}
|
||||
|
||||
[HttpGet("refresh")]
|
||||
public async Task<IActionResult> Refresh() {
|
||||
var refreshToken = Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return BadRequest("Refresh token not provided");
|
||||
|
||||
var token = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (token is null)
|
||||
return NotFound("Refresh token not valid");
|
||||
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
}
|
||||
|
||||
[HttpDelete("logout")]
|
||||
public IActionResult Logout() {
|
||||
accessor.Logout();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/auth")]
|
||||
public class AuthController(IAuthLogic auth) : ControllerBase {
|
||||
[Route("api/v1/authentication")]
|
||||
public class SecurityController(IAuthLogic auth) : ControllerBase {
|
||||
|
||||
[HttpPut("login")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
||||
@@ -1,83 +0,0 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/users")]
|
||||
public class UserController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IUserLogic logic) : ControllerBase {
|
||||
|
||||
private async Task<bool> AuthorizeRequest(string permission) {
|
||||
return await perms.HasPermission(context.AccessToken, permission);
|
||||
}
|
||||
|
||||
[HttpGet, Authorized]
|
||||
public async Task<ActionResult<IList<User>>> GetUsers() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUsers();
|
||||
}
|
||||
|
||||
[HttpGet("{userId}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUser(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUser(userId);
|
||||
}
|
||||
|
||||
[HttpGet("username/{username}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUserByUsername(string username) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUserByUsername(username);
|
||||
}
|
||||
|
||||
[HttpGet("email/{email}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUserByEmail(string email) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUserByEmail(email);
|
||||
}
|
||||
|
||||
[HttpPost, Authorized]
|
||||
public async Task<ActionResult<User>> CreateUser([FromBody] UserCreator user) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Create))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.CreateUser(user);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}"), Authorized]
|
||||
public async Task<ActionResult<User>> UpdateUser(string userId, [FromBody] User user) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.UpdateUser(userId, user);
|
||||
}
|
||||
|
||||
[HttpDelete("{userId}"), Authorized]
|
||||
public async Task<ActionResult> DeleteUser(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Delete))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.DeleteUser(userId);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/password"), Authorized]
|
||||
public async Task<ActionResult> ChangePassword(string userId, [FromBody] UserPasswordChange passwordChange) {
|
||||
if (context.User.Id.ToString() != userId && !await AuthorizeRequest(permissions.Value.Users.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.UpdatePassword(userId, passwordChange.OldPassword, passwordChange.NewPassword);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,4 +83,4 @@ public static class MvcExtensions {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@ using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Logic.Implementation;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
@@ -17,43 +15,23 @@ public static class ServiceCollectionExtensions {
|
||||
/// Adds all HopFrame endpoints and services to the application
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||
var controllers = new List<Type> { typeof(UserController), typeof(GroupController) };
|
||||
|
||||
var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication");
|
||||
if (!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication"))
|
||||
controllers.Add(typeof(AuthController));
|
||||
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled")) {
|
||||
IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback;
|
||||
controllers.Add(typeof(OpenIdController));
|
||||
}
|
||||
|
||||
AddHopFrameNoEndpoints<TDbContext>(services, configuration);
|
||||
services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
|
||||
AddHopFrameNoEndpoints<TDbContext>(services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HopFrame services to the application
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, 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);
|
||||
});
|
||||
|
||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||
services.AddScoped<IUserLogic, UserLogic>();
|
||||
services.AddScoped<IGroupLogic, GroupLogic>();
|
||||
|
||||
services.AddHopFrameAuthentication(configuration);
|
||||
services.AddHopFrameAuthentication();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Api</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -21,10 +22,4 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Api</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public interface IGroupLogic {
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetGroups();
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups();
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string userId);
|
||||
Task<LogicResult<PermissionGroup>> GetGroup(string name);
|
||||
|
||||
Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group);
|
||||
Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group);
|
||||
Task<LogicResult> DeleteGroup(string name);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public interface IUserLogic {
|
||||
Task<LogicResult<IList<User>>> GetUsers();
|
||||
Task<LogicResult<User>> GetUser(string id);
|
||||
Task<LogicResult<User>> GetUserByUsername(string username);
|
||||
Task<LogicResult<User>> GetUserByEmail(string email);
|
||||
|
||||
Task<LogicResult<User>> CreateUser(UserCreator user);
|
||||
Task<LogicResult<User>> UpdateUser(string id, User user);
|
||||
Task<LogicResult> DeleteUser(string id);
|
||||
Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword);
|
||||
}
|
||||
@@ -5,15 +5,12 @@ using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
|
||||
public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var user = await users.GetUserByEmail(login.Email);
|
||||
|
||||
if (user is null)
|
||||
@@ -25,25 +22,23 @@ internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens,
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (register.Password.Length < 8)
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
|
||||
|
||||
var allUsers = await users.GetUsers();
|
||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
||||
@@ -58,48 +53,46 @@ internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens,
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided");
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
||||
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token.Type != Token.RefreshTokenType)
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
|
||||
|
||||
if (token is null)
|
||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||
|
||||
if (token.Type != Token.RefreshTokenType)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token");
|
||||
|
||||
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
|
||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
||||
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult> Logout() {
|
||||
@@ -107,7 +100,9 @@ internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens,
|
||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
||||
await tokens.DeleteUserTokens(tokenContext.User);
|
||||
return LogicResult.Conflict("access or refresh token not provided");
|
||||
|
||||
await tokens.DeleteUserTokens(tokenContext.User);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
internal sealed class GroupLogic(IGroupRepository groups, IUserRepository users) : IGroupLogic {
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetGroups() {
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetPermissionGroups());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups() {
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetDefaultGroups());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<IList<PermissionGroup>>.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<IList<PermissionGroup>>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetUserGroups(user));
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> GetGroup(string name) {
|
||||
var group = await groups.GetPermissionGroup(name);
|
||||
|
||||
if (group is null)
|
||||
return LogicResult<PermissionGroup>.NotFound("That group does not exist");
|
||||
|
||||
return LogicResult<PermissionGroup>.Ok(group);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group) {
|
||||
if (group is null)
|
||||
return LogicResult<PermissionGroup>.BadRequest("Provide a group");
|
||||
|
||||
if (!group.Name.StartsWith("group."))
|
||||
return LogicResult<PermissionGroup>.BadRequest("Group names must start with 'group.'");
|
||||
|
||||
if (await groups.GetPermissionGroup(group.Name) != null)
|
||||
return LogicResult<PermissionGroup>.Conflict("That group already exists");
|
||||
|
||||
return LogicResult<PermissionGroup>.Ok(await groups.CreatePermissionGroup(group));
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group) {
|
||||
if (await groups.GetPermissionGroup(group.Name) == null)
|
||||
return LogicResult<PermissionGroup>.NotFound("That user does not exist");
|
||||
|
||||
await groups.EditPermissionGroup(group);
|
||||
return LogicResult<PermissionGroup>.Ok(group);
|
||||
}
|
||||
|
||||
public async Task<LogicResult> DeleteGroup(string name) {
|
||||
var group = await groups.GetPermissionGroup(name);
|
||||
|
||||
if (group is null)
|
||||
return LogicResult.NotFound("That group does not exist");
|
||||
|
||||
await groups.DeletePermissionGroup(group);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Claims;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
internal sealed class UserLogic(IUserRepository users, ITokenContext context) : IUserLogic {
|
||||
public async Task<LogicResult<IList<User>>> GetUsers() {
|
||||
return LogicResult<IList<User>>.Ok(await users.GetUsers());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUser(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<User>.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUserByUsername(string username) {
|
||||
var user = await users.GetUserByUsername(username);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUserByEmail(string email) {
|
||||
var user = await users.GetUserByEmail(email);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> CreateUser(UserCreator user) {
|
||||
var createdUser = new User {
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
Password = user.Password,
|
||||
};
|
||||
createdUser.Permissions = user.Permissions?.Select(p => new Permission {
|
||||
GrantedAt = DateTime.Now,
|
||||
PermissionName = p,
|
||||
User = createdUser
|
||||
}).ToList();
|
||||
|
||||
var newUser = await users.AddUser(createdUser);
|
||||
|
||||
if (newUser is null)
|
||||
return LogicResult<User>.Conflict("That user already exists");
|
||||
|
||||
return LogicResult<User>.Ok(newUser);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> UpdateUser(string id, User user) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<User>.BadRequest("Invalid user id");
|
||||
|
||||
if (user.Id != userId)
|
||||
return LogicResult<User>.Conflict("Cannot edit user with different user id");
|
||||
|
||||
if (await users.GetUser(userId) is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
await users.UpdateUser(user);
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult> DeleteUser(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult.NotFound("That user does not exist");
|
||||
|
||||
await users.DeleteUser(user);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public async Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult.NotFound("That user does not exist");
|
||||
|
||||
if (userId == context.User.Id && !await users.CheckUserPassword(user, oldPassword))
|
||||
return LogicResult.Conflict("Old password is not correct");
|
||||
|
||||
await users.ChangePassword(user, newPassword);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public class UserCreator {
|
||||
public string Username { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public virtual List<string> Permissions { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public class UserPasswordChange {
|
||||
public string OldPassword { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
}
|
||||
@@ -30,10 +30,5 @@ public abstract class HopDbContextBase : DbContext {
|
||||
.HasMany(g => g.Permissions)
|
||||
.WithOne(p => p.Group)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Token>()
|
||||
.HasMany(t => t.Permissions)
|
||||
.WithOne(t => t.Token)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Database</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -21,10 +22,4 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Database</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,9 +21,6 @@ public class Permission {
|
||||
[ForeignKey("GroupName"), JsonIgnore]
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
|
||||
[ForeignKey("TokenId"), JsonIgnore]
|
||||
public virtual Token Token { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public interface IPermissionOwner;
|
||||
|
||||
@@ -4,33 +4,24 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public class Token : IPermissionOwner {
|
||||
public class Token {
|
||||
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; }
|
||||
public Guid Content { 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; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace HopFrame.Database.Models;
|
||||
|
||||
public class User : IPermissionOwner {
|
||||
|
||||
[Key, Required]
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
public Guid Id { get; init; }
|
||||
|
||||
[Required, MaxLength(50)]
|
||||
@@ -14,7 +14,7 @@ public class User : IPermissionOwner {
|
||||
[Required, MaxLength(50), EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[MinLength(8), MaxLength(255), JsonIgnore]
|
||||
[Required, MinLength(8), MaxLength(255), JsonIgnore]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
@@ -5,7 +5,5 @@ 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);
|
||||
Task DeleteUserTokens(User owner);
|
||||
}
|
||||
@@ -33,38 +33,19 @@ internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRe
|
||||
}
|
||||
|
||||
public async Task EditPermissionGroup(PermissionGroup group) {
|
||||
var orig = await context.Groups
|
||||
.Include(g => g.Permissions) // Include related entities
|
||||
.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||
|
||||
var orig = await context.Groups.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;
|
||||
var entity = context.Groups.Update(orig);
|
||||
|
||||
// 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
|
||||
}
|
||||
entity.Entity.IsDefaultGroup = group.IsDefaultGroup;
|
||||
entity.Entity.Description = group.Description;
|
||||
entity.Entity.Permissions = group.Permissions;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group) {
|
||||
group.CreatedAt = DateTime.Now;
|
||||
await context.Groups.AddAsync(group);
|
||||
|
||||
@@ -5,10 +5,6 @@ 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) {
|
||||
@@ -28,12 +24,6 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
entry.User = user;
|
||||
}else if (owner is PermissionGroup group) {
|
||||
entry.Group = group;
|
||||
}else if (owner is Token token) {
|
||||
if (token.Type != Token.ApiTokenType)
|
||||
throw new ArgumentException("Only API tokens can have permissions!");
|
||||
if (!await HasPermission(token.Owner, permission))
|
||||
throw new ArgumentException("An api token cannot have more permissions than the owner has!");
|
||||
entry.Token = token;
|
||||
}
|
||||
|
||||
await context.Permissions.AddAsync(entry);
|
||||
@@ -58,13 +48,6 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}else if (owner is Token token) {
|
||||
entry = await context.Permissions
|
||||
.Include(p => p.Token)
|
||||
.Where(p => p.Token != null)
|
||||
.Where(p => p.Token.TokenId == token.TokenId)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (entry is not null) {
|
||||
@@ -75,10 +58,6 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
|
||||
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
|
||||
var permissions = new List<string>();
|
||||
|
||||
if (owner is Token token && token.Type != Token.ApiTokenType) {
|
||||
owner = token.Owner;
|
||||
}
|
||||
|
||||
if (owner is User user) {
|
||||
var perms = await context.Permissions
|
||||
@@ -95,14 +74,6 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}else if (owner is Token apiToken) {
|
||||
var perms = await context.Permissions
|
||||
.Include(p => p.Token)
|
||||
.Where(p => p.Token != null)
|
||||
.Where(p =>p.Token.TokenId == apiToken.TokenId)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
@@ -12,14 +11,14 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
||||
|
||||
return await context.Tokens
|
||||
.Include(t => t.Owner)
|
||||
.Where(t => t.TokenId == guid)
|
||||
.Where(t => t.Content == guid)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Token> CreateToken(int type, User owner) {
|
||||
var token = new Token {
|
||||
CreatedAt = DateTime.Now,
|
||||
TokenId = Guid.NewGuid(),
|
||||
Content = Guid.NewGuid(),
|
||||
Type = type,
|
||||
Owner = owner
|
||||
};
|
||||
@@ -30,37 +29,13 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
|
||||
public async Task DeleteUserTokens(User owner) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -69,45 +69,10 @@ internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepos
|
||||
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||
if (entry is null) return;
|
||||
|
||||
// Update the main entity's properties
|
||||
entry.Email = user.Email;
|
||||
entry.Username = user.Username;
|
||||
|
||||
// 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
|
||||
}
|
||||
entry.Permissions = user.Permissions;
|
||||
entry.Tokens = user.Tokens;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
15
src/HopFrame.Security/AdminPermissions.cs
Normal file
15
src/HopFrame.Security/AdminPermissions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace HopFrame.Security;
|
||||
|
||||
public static class AdminPermissions {
|
||||
public const string IsAdmin = "hopframe.admin";
|
||||
|
||||
public const string ViewUsers = "hopframe.admin.users.view";
|
||||
public const string EditUser = "hopframe.admin.users.edit";
|
||||
public const string DeleteUser = "hopframe.admin.users.delete";
|
||||
public const string AddUser = "hopframe.admin.users.add";
|
||||
|
||||
public const string ViewGroups = "hopframe.admin.groups.view";
|
||||
public const string EditGroup = "hopframe.admin.groups.edit";
|
||||
public const string DeleteGroup = "hopframe.admin.groups.delete";
|
||||
public const string AddGroup = "hopframe.admin.groups.add";
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -20,83 +17,39 @@ public class HopFrameAuthentication(
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
ITokenRepository tokens,
|
||||
IPermissionRepository perms,
|
||||
IOptions<HopFrameAuthenticationOptions> tokenOptions,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IUserRepository users,
|
||||
IOpenIdAccessor accessor)
|
||||
IPermissionRepository perms)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||
|
||||
public const string SchemeName = "HopFrame.Authentication";
|
||||
public const string SchemeName = "HopCore.Authentication";
|
||||
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
|
||||
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||
var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled && !Guid.TryParse(accessToken, out _)) {
|
||||
var result = await accessor.InspectToken(accessToken);
|
||||
|
||||
if (result is null || !result.Active)
|
||||
return AuthenticateResult.Fail("Invalid OpenID Connect token");
|
||||
|
||||
var email = result.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return AuthenticateResult.Fail("OpenID user has no email associated to it");
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return AuthenticateResult.Fail("OpenID user does not exist");
|
||||
|
||||
var username = result.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
var token = new Token {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
var identity = await GenerateClaims(token, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
|
||||
}
|
||||
|
||||
if (!tokenOptions.Value.DefaultAuthentication)
|
||||
return AuthenticateResult.Fail("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||
|
||||
if (tokenEntry.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.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
|
||||
|
||||
if (tokenEntry.Owner is null)
|
||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||
|
||||
var principal = await GenerateClaims(tokenEntry, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
}
|
||||
|
||||
public static async Task<ClaimsPrincipal> GenerateClaims(Token token, IPermissionRepository perms) {
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(token);
|
||||
var permissions = await perms.GetFullPermissions(tokenEntry.Owner);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
var principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
||||
return principal;
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +1,23 @@
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Security.Authentication;
|
||||
|
||||
public static class HopFrameAuthenticationExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
|
||||
/// </summary>
|
||||
/// <param name="service">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) {
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) {
|
||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
service.AddHttpClient();
|
||||
service.AddMemoryCache();
|
||||
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||
|
||||
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
|
||||
|
||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
service.AddAuthorization();
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication;
|
||||
|
||||
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Authentication";
|
||||
|
||||
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
|
||||
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : RefreshToken.ConstructTimeSpan;
|
||||
|
||||
public bool DefaultAuthentication { get; set; } = true;
|
||||
|
||||
public TokenTime AccessToken { get; set; }
|
||||
public TokenTime RefreshToken { get; set; }
|
||||
|
||||
public class TokenTime {
|
||||
public int Days { get; set; }
|
||||
public int Hours { get; set; }
|
||||
public int Minutes { get; set; }
|
||||
public int Seconds { get; set; }
|
||||
|
||||
public TimeSpan ConstructTimeSpan => new(Days, Hours, Minutes, Seconds);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface IOpenIdAccessor {
|
||||
public static string DefaultCallback;
|
||||
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
Task<OpenIdToken> RequestToken(string code);
|
||||
Task<string> ConstructAuthUri(string state = null);
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
void SetAuthenticationCookies(OpenIdToken token);
|
||||
void Logout();
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.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);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdConfiguration {
|
||||
[JsonPropertyName("issuer")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("authorization_endpoint")]
|
||||
public string AuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint")]
|
||||
public string TokenEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("userinfo_endpoint")]
|
||||
public string UserinfoEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("end_session_endpoint")]
|
||||
public string EndSessionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("introspection_endpoint")]
|
||||
public string IntrospectionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("revocation_endpoint")]
|
||||
public string RevocationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("device_authorization_endpoint")]
|
||||
public string DeviceAuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("response_types_supported")]
|
||||
public List<string> ResponseTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("response_modes_supported")]
|
||||
public List<string> ResponseModesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("jwks_uri")]
|
||||
public string JwksUri { get; set; }
|
||||
|
||||
[JsonPropertyName("grant_types_supported")]
|
||||
public List<string> GrantTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
||||
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("subject_types_supported")]
|
||||
public List<string> SubjectTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint_auth_methods_supported")]
|
||||
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("acr_values_supported")]
|
||||
public List<string> AcrValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("scopes_supported")]
|
||||
public List<string> ScopesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("request_parameter_supported")]
|
||||
public bool RequestParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_supported")]
|
||||
public List<string> ClaimsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_parameter_supported")]
|
||||
public bool ClaimsParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("code_challenge_methods_supported")]
|
||||
public List<string> CodeChallengeMethodsSupported { get; set; }
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdIntrospection {
|
||||
[JsonPropertyName("iss")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("sub")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("aud")]
|
||||
public string Audience { get; set; }
|
||||
|
||||
[JsonPropertyName("exp")]
|
||||
public long Expiration { get; set; }
|
||||
|
||||
[JsonPropertyName("iat")]
|
||||
public long IssuedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("auth_time")]
|
||||
public long AuthTime { get; set; }
|
||||
|
||||
[JsonPropertyName("acr")]
|
||||
public string Acr { get; set; }
|
||||
|
||||
[JsonPropertyName("amr")]
|
||||
public List<string> AuthenticationMethods { get; set; }
|
||||
|
||||
[JsonPropertyName("sid")]
|
||||
public string SessionId { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonPropertyName("email_verified")]
|
||||
public bool EmailVerified { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("given_name")]
|
||||
public string GivenName { get; set; }
|
||||
|
||||
[JsonPropertyName("preferred_username")]
|
||||
public string PreferredUsername { get; set; }
|
||||
|
||||
[JsonPropertyName("nickname")]
|
||||
public string Nickname { get; set; }
|
||||
|
||||
[JsonPropertyName("groups")]
|
||||
public List<string> Groups { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public bool Active { get; set; }
|
||||
|
||||
[JsonPropertyName("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
[JsonPropertyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdToken {
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[JsonPropertyName("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Options;
|
||||
|
||||
public sealed class OpenIdOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Authentication:OpenID";
|
||||
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool GenerateUsers { get; set; } = true;
|
||||
|
||||
public string Issuer { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string Callback { get; set; }
|
||||
|
||||
public HopFrameAuthenticationOptions.TokenTime RefreshToken { get; set; } = new() {
|
||||
Days = 30
|
||||
};
|
||||
|
||||
public CachingOptions Cache { get; set; } = new() {
|
||||
Enabled = true,
|
||||
Configuration = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Hours = 24
|
||||
}
|
||||
},
|
||||
Auth = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Seconds = 30
|
||||
}
|
||||
},
|
||||
Inspection = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Minutes = 2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public class CachingTypeOptions {
|
||||
public bool Enabled { get; set; }
|
||||
public HopFrameAuthenticationOptions.TokenTime TTL { get; set; }
|
||||
}
|
||||
|
||||
public class CachingOptions {
|
||||
public bool Enabled { get; set; }
|
||||
public CachingTypeOptions Configuration { get; set; }
|
||||
public CachingTypeOptions Auth { get; set; }
|
||||
public CachingTypeOptions Inspection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authorization;
|
||||
|
||||
public class AdminPermissionOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Permissions";
|
||||
|
||||
public string Dashboard { get; set; } = "hopframe.admin";
|
||||
|
||||
public CrudPermission Users { get; set; } = new() {
|
||||
Read = "hopframe.admin.users.read",
|
||||
Update = "hopframe.admin.users.update",
|
||||
Delete = "hopframe.admin.users.delete",
|
||||
Create = "hopframe.admin.users.create"
|
||||
};
|
||||
|
||||
public CrudPermission Groups { get; set; } = new() {
|
||||
Read = "hopframe.admin.groups.read",
|
||||
Update = "hopframe.admin.groups.update",
|
||||
Delete = "hopframe.admin.groups.delete",
|
||||
Create = "hopframe.admin.groups.create"
|
||||
};
|
||||
|
||||
public class CrudPermission {
|
||||
public string Create { get; set; }
|
||||
public string Read { get; set; }
|
||||
public string Update { get; set; }
|
||||
public string Delete { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Claims;
|
||||
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext {
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext {
|
||||
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
|
||||
|
||||
public Token AccessToken => options.Value.Enabled ? new Token {
|
||||
Owner = User,
|
||||
Type = Token.OpenIdTokenType,
|
||||
CreatedAt = DateTime.Now
|
||||
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
<RootNamespace>HopFrame.Security</RootNamespace>
|
||||
|
||||
<PackageId>HopFrame.Security</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace HopFrame.Security.Options;
|
||||
|
||||
public abstract class OptionsFromConfiguration {
|
||||
public abstract string Position { get; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Security.Options;
|
||||
|
||||
public static class OptionsFromConfigurationExtensions {
|
||||
public static void AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration) where T : OptionsFromConfiguration {
|
||||
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||
string position = optionsInstance?.Position;
|
||||
if (position is null) {
|
||||
throw new ArgumentException($"""Configuration "{typeof(T).Name}" has no position configured!""");
|
||||
}
|
||||
|
||||
services.Configure((Action<T>)(options => {
|
||||
IConfigurationSection section = configuration.GetSection(position);
|
||||
section.Bind(options);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ public sealed class AdminPermissionsAttribute(string view = null, string create
|
||||
Create = create,
|
||||
Update = update,
|
||||
Delete = delete,
|
||||
Read = view
|
||||
View = view
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public interface IAdminPageGenerator<TModel> {
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ReadPermission(string permission);
|
||||
IAdminPageGenerator<TModel> ViewPermission(string permission);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to create a new Entry
|
||||
|
||||
@@ -48,8 +48,8 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ReadPermission(string permission) {
|
||||
Page.Permissions.Read = permission;
|
||||
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
|
||||
Page.Permissions.View = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
|
||||
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
|
||||
CreatePermission(attribute?.Permissions.Create);
|
||||
UpdatePermission(attribute?.Permissions.Update);
|
||||
ReadPermission(attribute?.Permissions.Read);
|
||||
ViewPermission(attribute?.Permissions.View);
|
||||
DeletePermission(attribute?.Permissions.Delete);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Web.Admin</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace HopFrame.Web.Admin.Models;
|
||||
|
||||
public sealed class AdminPagePermissions {
|
||||
public string Read { get; set; }
|
||||
public string View { get; set; }
|
||||
public string Create { get; set; }
|
||||
public string Update { get; set; }
|
||||
public string Delete { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -19,10 +20,16 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
|
||||
next?.Invoke(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var principal = await HopFrameAuthentication.GenerateClaims(token, perms);
|
||||
if (principal?.Identity is ClaimsIdentity identity)
|
||||
context.User.AddIdentity(identity);
|
||||
var permissions = await perms.GetFullPermissions(token.Owner);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
||||
}
|
||||
|
||||
await next?.Invoke(context);
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
|
||||
private async void Save() {
|
||||
if (_isEdit && _currentPage.Permissions.Update is not null) {
|
||||
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Update)) {
|
||||
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to edit an entry!",
|
||||
@@ -330,7 +330,7 @@
|
||||
return;
|
||||
}
|
||||
}else if (_currentPage.Permissions.Create is not null) {
|
||||
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Create)) {
|
||||
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to add an entry!",
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Security;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
using HopFrame.Web.Provider;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : AdminPagesContext {
|
||||
internal class HopAdminContext : AdminPagesContext {
|
||||
|
||||
[AdminPageUrl("users")]
|
||||
public AdminPage<User> Users { get; set; }
|
||||
@@ -23,10 +21,10 @@ internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : Admin
|
||||
generator.Page<User>()
|
||||
.Description("On this page you can manage all user accounts.")
|
||||
.ConfigureProvider<UserProvider>()
|
||||
.ReadPermission(options.Value.Users.Read)
|
||||
.CreatePermission(options.Value.Users.Create)
|
||||
.UpdatePermission(options.Value.Users.Update)
|
||||
.DeletePermission(options.Value.Users.Delete);
|
||||
.ViewPermission(AdminPermissions.ViewUsers)
|
||||
.CreatePermission(AdminPermissions.AddUser)
|
||||
.UpdatePermission(AdminPermissions.EditUser)
|
||||
.DeletePermission(AdminPermissions.DeleteUser);
|
||||
|
||||
generator.Page<User>().Property(u => u.Password)
|
||||
.DisplayInListing(false)
|
||||
@@ -66,10 +64,10 @@ internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : Admin
|
||||
generator.Page<PermissionGroup>()
|
||||
.Description("On this page you can view, create, edit and delete permission groups.")
|
||||
.ConfigureProvider<GroupProvider>()
|
||||
.ReadPermission(options.Value.Groups.Read)
|
||||
.CreatePermission(options.Value.Groups.Create)
|
||||
.UpdatePermission(options.Value.Groups.Update)
|
||||
.DeletePermission(options.Value.Groups.Delete)
|
||||
.ViewPermission(AdminPermissions.ViewGroups)
|
||||
.CreatePermission(AdminPermissions.AddGroup)
|
||||
.UpdatePermission(AdminPermissions.EditGroup)
|
||||
.DeletePermission(AdminPermissions.DeleteGroup)
|
||||
.ListingProperty(g => g.Name);
|
||||
|
||||
generator.Page<PermissionGroup>().Property(g => g.Name)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
|
||||
<PackageId>HopFrame.Web</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -34,10 +35,4 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Web</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public class HopFrameWebModuleConfig {
|
||||
public string AdminLoginPageUri { get; set; } = "/administration/login";
|
||||
}
|
||||
@@ -5,27 +5,25 @@
|
||||
@using BlazorStrap
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Security.Authorization
|
||||
@using HopFrame.Security
|
||||
@using HopFrame.Web.Admin.Providers
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.Extensions.Options
|
||||
@layout AdminLayout
|
||||
|
||||
<AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="@ConstructRedirectUri()" />
|
||||
<AuthorizedView Permission="@AdminPermissions.IsAdmin" RedirectIfUnauthorized="/administration/login" />
|
||||
|
||||
<PageTitle>Admin Dashboard</PageTitle>
|
||||
|
||||
<BSContainer>
|
||||
<BSRow Justify="Justify.Center">
|
||||
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||
<AuthorizedView Permission="@adminPage.Permissions.Read">
|
||||
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||
<BSCol Column="4" style="margin-bottom: 10px">
|
||||
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px; min-width: 200px">
|
||||
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
|
||||
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
|
||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.Read</span></BSCard>
|
||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
|
||||
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
|
||||
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
|
||||
</BSCard>
|
||||
@@ -38,17 +36,11 @@
|
||||
|
||||
@inject NavigationManager Navigator
|
||||
@inject IAdminPagesProvider Pages
|
||||
@inject IOptions<AdminPermissionOptions> Options
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
|
||||
public void NavigateTo(string url) {
|
||||
Navigator.NavigateTo("/administration/" + url, true);
|
||||
}
|
||||
|
||||
public string ConstructRedirectUri() {
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration";
|
||||
Navigator.NavigateTo("administration/" + url, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
private UserLogin UserLogin { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "redirect")]
|
||||
public string RedirectAfter { get; set; }
|
||||
private string RedirectAfter { get; set; }
|
||||
|
||||
private const string DefaultRedirect = "/administration";
|
||||
|
||||
@@ -65,6 +65,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true);
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : "/administration/" + RedirectAfter, true);
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,9 @@
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Web.Admin
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
|
||||
<PageTitle>@_pageData.Title</PageTitle>
|
||||
<AuthorizedView Permission="@_pageData.Permissions.Read" RedirectIfUnauthorized="@GenerateRedirectString()" />
|
||||
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="@GenerateRedirectString()" />
|
||||
|
||||
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>
|
||||
|
||||
@@ -34,7 +33,7 @@
|
||||
<div class="d-flex" role="search" id="search">
|
||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @oninput="TriggerSearch">
|
||||
</div>
|
||||
<AuthorizedView Permission="@_pageData.Permissions.Create">
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
|
||||
<BSButton IsSubmit="false" Color="BSColor.Success" @onclick="Create">Add Entry</BSButton>
|
||||
</AuthorizedView>
|
||||
</div>
|
||||
@@ -108,7 +107,6 @@
|
||||
@inject IPermissionRepository Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject NavigationManager Navigator
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -142,8 +140,8 @@
|
||||
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
|
||||
_modelProvider = _pageData.LoadModelProvider(Provider);
|
||||
|
||||
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update);
|
||||
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete);
|
||||
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
|
||||
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
|
||||
|
||||
await Reload();
|
||||
}
|
||||
@@ -253,6 +251,6 @@
|
||||
}
|
||||
|
||||
private string GenerateRedirectString() {
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration/" + _pageData?.Url;
|
||||
return "/administration/login?redirect=" + _pageData?.Url;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
|
||||
|
||||
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||
<AuthorizedView Permission="@adminPage.Permissions.Read">
|
||||
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
|
||||
</AuthorizedView>
|
||||
}
|
||||
|
||||
@@ -3,29 +3,26 @@ using CurrieTechnologies.Razor.SweetAlert2;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Models;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddHttpClient();
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddTransient<AuthMiddleware>();
|
||||
services.AddAdminContext<HopAdminContext>();
|
||||
services.AddSingleton(config ?? new HopFrameWebModuleConfig());
|
||||
|
||||
// Component library's
|
||||
services.AddSweetAlert2();
|
||||
services.AddBlazorStrap();
|
||||
|
||||
services.AddHopFrameAuthentication(configuration);
|
||||
services.AddHopFrameAuthentication();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
@@ -14,16 +11,10 @@ internal class AuthService(
|
||||
IUserRepository userService,
|
||||
IHttpContextAccessor httpAccessor,
|
||||
ITokenRepository tokens,
|
||||
ITokenContext context,
|
||||
IOptions<HopFrameAuthenticationOptions> options,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IOpenIdAccessor accessor,
|
||||
IUserRepository users)
|
||||
ITokenContext context)
|
||||
: IAuthService {
|
||||
|
||||
public async Task Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return;
|
||||
|
||||
var user = await userService.AddUser(new User {
|
||||
Username = register.Username,
|
||||
Email = register.Email,
|
||||
@@ -35,21 +26,19 @@ internal class AuthService(
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return false;
|
||||
|
||||
var user = await userService.GetUserByEmail(login.Email);
|
||||
|
||||
if (user == null) return false;
|
||||
@@ -58,13 +47,13 @@ internal class AuthService(
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
@@ -84,51 +73,16 @@ internal class AuthService(
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
|
||||
|
||||
if (openIdOptions.Value.Enabled && !Guid.TryParse(refreshToken, out _)) {
|
||||
var openIdToken = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (openIdToken is null)
|
||||
return null;
|
||||
|
||||
var inspection = await accessor.InspectToken(openIdToken.AccessToken);
|
||||
|
||||
var email = inspection.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return null;
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return null;
|
||||
|
||||
var username = inspection.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
accessor.SetAuthenticationCookies(openIdToken);
|
||||
return new() {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
}
|
||||
|
||||
if (!options.Value.DefaultAuthentication)
|
||||
return null;
|
||||
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token is null || token.Type != Token.RefreshTokenType) return null;
|
||||
|
||||
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) return null;
|
||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null;
|
||||
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
@@ -137,12 +91,15 @@ internal class AuthService(
|
||||
}
|
||||
|
||||
public async Task<bool> IsLoggedIn() {
|
||||
var accessToken = context.AccessToken;
|
||||
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) return false;
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (accessToken is null) return false;
|
||||
if (accessToken.Type != Token.AccessTokenType && accessToken.Type != Token.OpenIdTokenType) return false;
|
||||
if (accessToken.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false;
|
||||
if (accessToken.Owner is null) return false;
|
||||
if (tokenEntry is null) return false;
|
||||
if (tokenEntry.Type != Token.AccessTokenType) return false;
|
||||
if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
|
||||
if (tokenEntry.Owner is null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using FrontendTest.Providers;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using HopFrame.Testing.Web.Providers;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Web;
|
||||
namespace FrontendTest;
|
||||
|
||||
public class AdminContext : AdminPagesContext {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="app.css"/>
|
||||
<link rel="stylesheet" href="HopFrame.Testing.Web.styles.css"/>
|
||||
<link rel="stylesheet" href="FrontendTest.styles.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
@@ -6,5 +6,5 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using HopFrame.Testing.Web
|
||||
@using HopFrame.Testing.Web.Components
|
||||
@using FrontendTest
|
||||
@using FrontendTest.Components
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Web;
|
||||
namespace FrontendTest;
|
||||
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
public DbSet<Employee> Employees { get; set; }
|
||||
@@ -11,7 +11,7 @@ public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
@@ -4,7 +4,6 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
namespace RestApiTest.Models;
|
||||
|
||||
public class Address {
|
||||
[ForeignKey("Employee")]
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
namespace RestApiTest.Models;
|
||||
|
||||
public class Employee {
|
||||
public int EmployeeId { get; set; }
|
||||
@@ -1,12 +1,12 @@
|
||||
using HopFrame.Testing.Web;
|
||||
using HopFrame.Testing.Web.Components;
|
||||
using FrontendTest;
|
||||
using FrontendTest.Components;
|
||||
using HopFrame.Web;
|
||||
using HopFrame.Web.Admin;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
builder.Services.AddAdminContext<AdminContext>();
|
||||
|
||||
// Add services to the container.
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Web.Admin;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Web.Providers;
|
||||
namespace FrontendTest.Providers;
|
||||
|
||||
public class AddressProvider(DatabaseContext context) : ModelProvider<Address> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Web.Admin;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Web.Providers;
|
||||
namespace FrontendTest.Providers;
|
||||
|
||||
public class EmployeeProvider(DatabaseContext context) : ModelProvider<Employee> {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,23 +1,20 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Api.Controllers;
|
||||
namespace RestApiTest.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("test")]
|
||||
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
|
||||
public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase {
|
||||
|
||||
[HttpGet("permissions"), Authorized]
|
||||
public async Task<ActionResult<IList<string>>> Permissions() {
|
||||
return new ActionResult<IList<string>>(await permissions.GetFullPermissions(userContext.AccessToken));
|
||||
public ActionResult<IList<Permission>> Permissions() {
|
||||
return new ActionResult<IList<Permission>>(userContext.User.Permissions);
|
||||
}
|
||||
|
||||
[HttpGet("generate")]
|
||||
@@ -53,24 +50,5 @@ public class TestController(ITokenContext userContext, DatabaseContext context,
|
||||
public async Task<ActionResult<IList<Address>>> GetAddresses() {
|
||||
return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("token"), Authorized]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> GetApiToken() {
|
||||
var token = await tokens.CreateApiToken(userContext.User, DateTime.MaxValue);
|
||||
await permissions.AddPermission(token, "hopframe.admin");
|
||||
await permissions.AddPermission(token, "hopframe.admin.users.read");
|
||||
return LogicResult<SingleValueResult<string>>.Ok(token.TokenId.ToString());
|
||||
}
|
||||
|
||||
[HttpDelete("token/{tokenId}")]
|
||||
public async Task DeleteToken(string tokenId) {
|
||||
var token = await tokens.GetToken(tokenId);
|
||||
await tokens.DeleteToken(token);
|
||||
}
|
||||
|
||||
[HttpGet("url")]
|
||||
public ActionResult<string> GetUrl() {
|
||||
return Ok(IOpenIdAccessor.DefaultCallback ?? "Not set");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace HopFrame.Testing.Api;
|
||||
namespace RestApiTest;
|
||||
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
|
||||
@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
namespace RestApiTest.Models;
|
||||
|
||||
public class Address {
|
||||
[ForeignKey("Employee")]
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
namespace RestApiTest.Models;
|
||||
|
||||
public class Employee {
|
||||
public int EmployeeId { get; set; }
|
||||
@@ -1,4 +1,4 @@
|
||||
using HopFrame.Testing.Api;
|
||||
using RestApiTest;
|
||||
using HopFrame.Api.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
@@ -6,9 +6,8 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
@@ -19,7 +18,7 @@ builder.Services.AddSwaggerGen(c => {
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
|
||||
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
|
||||
Enter 'Bearer' [space] and then your token in the text input below.",
|
||||
Name = "Token",
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "Bearer"
|
||||
@@ -4,7 +4,6 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user